百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Java基础——Java多线程(进程与线程间通信方式)

sinye56 2024-11-24 21:30 1 浏览 0 评论

1 基本概括

2 主要介绍

2.1 进程通信和线程通信的概念

进程通信

进程相互交换数据与信息。进程间通信有两种基本模型:共享内存和消息传递(消息队列)。

线程通信

原因:为了更好地协作,线程无论是交替式执行,还是接力式执行,都需要进行通信告知。

线程间通信:

(1)临界区

通过多线程的串行化允许线程对共享资源的访问,速度快

(2)互斥量

只有拥有互斥对象的线程才能对资源空间进行访问,因为互斥对象只有一个,所以可以保证公共资源不被多个线程访问

(3)信号量

用于控制多个线程对共享空间资源的访问,一般会限制同一时刻访问资源的最大线程数

(4)信号

通过通知操作的方式来控制线程间的同步,可以区分线程间的优先级

2.2 线程通信

2.2.1线程通信方式

共享内存:

共享内存这种方式比较常见,我们经常会设置一个共享变量。然后多个线程去操作同一个共享变量。从而达到线程通讯的目的。例如,我们使用多个线程去执行页面抓取任务,我们可以使用一个共享变量count来记录任务完成的数量。每当一个线程完成抓取任务,会在原来的count上执行加1操作。这样每个线程都可以通过获取这个count变量来获得当前任务的完成情况。当然必须要考虑的是共享变量的同步问题,这也共享内存容易出错的原因所在。

这种通讯模型中,不同的线程之间是没有直接联系的。都是通过共享变量这个“中间人”来进行交互。而这个“中间人”必要情况下还需被保护在临界区内(加锁或同步)。由此可见,一旦共享变量变得多起来,并且涉及到多种不同线程对象的交互,这种管理会变得非常复杂,极容易出现死锁等问题。

消息传递:

消息传递方式采取的是线程之间的直接通信,不同的线程之间通过显式的发送消息来达到交互目的。消息传递最有名的方式应该是actor模型了。在这种模型下,一切都是actor,所有的actor之间的通信都必须通过传递消息才能达到。每个actor都有一个收件箱(消息队列)用来保存收到其他actor传递来的消息。actor自己也可以给自己发送消息。这才是面向对象的精髓啊!

这种模型看起来比共享内存模型要复杂。但是一旦碰到复杂业务的话,actor模型的优势就体现出来了。

首先我们定义一个统计actor用来统计任务完成量。然后把多个网址(消息方式)发给多个抓取actor,抓取actor处理完任务后发送消息通知统计actor任务完成,统计actor对自己保存的变量count(这个只有统计actor才能看到)加一。

2.2.2 线程通信的具体方式

1.volatile关键字方式

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

volatile语义保证线程可见性有两个原则保证

所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存

所有volatile修饰的变量在使用之前必须重新读取主内存的值

volatile保证可见性原理图

如果将volatile关键字去掉,线程切换一定次数后将不能感知到flag的变化,最开始能感知是线程启动时间差的原因。

2.等待/通知机制

等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。

为什么要必须获取锁?

因为调用wait方法时,必须要先释放锁,如果没有持有锁将会抛出异常。

wait()方法和notify()方法和notifyAll()方法

1 .wait()方法 语义:使得当前线程立刻停止运行,处于等待状态(WAIT),并将当前线程置入锁对象的等待队列中,直到被通知(notify)或被中断为止。 使用条件:wait方法只能在同步方法或同步代码块中使用...

2.notify()方法 语义:唤醒处于等待状态的线程 使用条件:notify()也必须在同步方法或同步代码块中调用,用来唤醒等待该...

3.notifyAll()方法 唤醒所有处于等待状态的线程

3.Join 方法

方法join()的作用是等待线程销毁,方法join具有使线程排队的作用,有些类似同步的运行效果,方法join的作用是使所属的线程对象x正常执行run方法的任务,而使当前线程z进行无限期的阻塞,等待线程

x执行完毕后再执行线程后面的代码。join与sychronized关键字的区别:jion在内部使用wait方法进行等待,而sychronized使用的是“对象监视器原理”作为同步。

join方法的源码

  public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
 
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
  }


4.管道

管道流是JAVA中线程通讯的常用方式之一,基本流程如下:

1)创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis

2)将pos和pis匹配,pos.connect(pis);

3)将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了

5.threadLocal方式

threadLocal方式的线程通信,不像以上四种方式是多个线程之间的通信,它更像是一个线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。

2.3 进程通信

2.3.1进程通信分类

低级通信

由于进程的互斥和同步,需要在进程间交换一定的信息,故不少学者将它们也归为进程通信。只能传递状态和整数值(控制信息)。

特点:传送信息量小,效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。

编程复杂:用户直接实现通信的细节,容易出现。

高级通信

提高信号通信的效率,传递大量数据,减轻程序编制的复杂度。

提供三种方式:

1.共享内存模式

2.消息传递模式

3.共享文件模式

2.3.2 进程通信方式

管道

(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。

(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

系统IPC

(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺

(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。

(7)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

套接口

(8)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

2.3.3 进程通信方式的优缺点

效率对比:

管道

a、较早的一种通信方式,缺点明显:只能用于有亲缘关系进程之间的通信;只支持单向数据流,如果要双向通信需要多创建一个管道来实现。

b、自身具备同步机制。

c、随进程持续。

信号

a、这种通信可携带的信息极少。不适合需要经常携带数据的通信。

b、不具备同步机制,类似于中断,什么时候产生信号,进程是不知道的。

消息队列

a、与共享内存和FIFO类似,使用一个路径名来实现各个无亲缘关系进程之间的通信。消息队列相比于其他方式有很多优点:它提供有格式的字节流,减少了开发人员的工作量;消息具有类型(system V)或优先级(posix)。其他方式都没有这些优点。

b、具备同步机制。

c、随内核持续。

共享内存

a、最快的一种通信方式,多个进程可同时访问同一片内存空间,相对其他方式来说具有更少的数据拷贝,效率较高。

b、需要结合信号灯或其他方式来实现多个进程间同步,自身不具备同步机制。

c、随内核持续,相比于随进程持续生命力更强。

FIFO

a、是有名管道,所以支持没有亲缘关系的进程通信。和共享内存类似,提供一个路径名字将各个无亲缘关系的进程关联起来。但是也需要创建两个描述符来实现双向通信。

b、自身具备同步机制。

c、随进程持续。

socket

a、使用socket通信的方式实现起来简单,可以使用因特网域和UNIX域来实现,使用因特网域可以实现不同主机之间的进出通信。

b、该方式自身携带同步机制,不需要额外的方式来辅助实现同步。

c、随进程持续。

3 实现方式

3.1 volatile实现多线程通信

public class VolatileDemo {
    private static volatile boolean flag = true;
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (flag){
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (!flag){
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}


3.2 等待/通知机制(wait/notify机制)

public class WaitDemo {
    private static Object lock = new Object();
    private static  boolean flag = true;
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (flag){
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    System.out.println("wait end ....... ");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag){
                    synchronized (lock){
                        if (flag){
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }

                    }
                }
            }
        }).start();
    }
}


3.3 join方法

/**
* Join方法就是挂起调用线程,直到被调用线程执行完毕后再继续执行。例:threadB线程中threadA的join方法,
* 所以threadB需在threadA执行完毕后才继续执行join后的代码,而主线程执行threadB.join(),所以最终主线程需等threadA和threadB执行完毕后才继续。
*
*/
public class JoinThread {

    public static void join() throws InterruptedException {
        long startTime = System.currentTimeMillis();
        Thread threadA = new Thread(() -> {
            try {
                log.info("threadA start");
                Thread.sleep(4000);
                log.info("threadA end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                threadA.join();
                log.info("threadB start");
                Thread.sleep(3000);
                log.info("threadB end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();


        threadB.join();
        log.info("run time [{}]", startTime - System.currentTimeMillis());
        log.info("main thread end");
    }
}


3.4 管道


public class testPipeConnection {

  public static void main(String[] args) {
    /**
     * 创建管道输出流
     */
    PipedOutputStream pos = new PipedOutputStream();
    /**
     * 创建管道输入流
     */
    PipedInputStream pis = new PipedInputStream();
    try {
      /**
       * 将管道输入流与输出流连接 此过程也可通过重载的构造函数来实现
       */
      pos.connect(pis);
    } catch (IOException e) {
      e.printStackTrace();
    }
    /**
     * 创建生产者线程
     */
    Producer p = new Producer(pos);
    /**
     * 创建消费者线程
     */
    Consumer1 c1 = new Consumer1(pis);
    /**
     * 启动线程
     */
    p.start();
    c1.start();
  }
}

/**
 * 生产者线程(与一个管道输入流相关联)
 * 
 */
class Producer extends Thread {
  private PipedOutputStream pos;

  public Producer(PipedOutputStream pos) {
    this.pos = pos;
  }

  public void run() {
    int i = 0;
    try {
      while(true)
      {
      this.sleep(3000);
      pos.write(i);
      i++;
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

/**
 * 消费者线程(与一个管道输入流相关联)
 * 
 */
class Consumer1 extends Thread {
  private PipedInputStream pis;

  public Consumer1(PipedInputStream pis) {
    this.pis = pis;
  }

  public void run() {
    try {
      while(true)
      {
      System.out.println("consumer1:"+pis.read());
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}


4 常见问题

1 进程间通信的方式有哪些?线程间通讯方式有哪些?
2 join方法的原理?
3 notify和notifyall有什么区别?
4 为什么wait方法要写在while循环里面而不是if呢?
5 在 Java 的并发编程中,什么是等待-通知机制?它是怎么实现的?


常见出现的问题会在后面的文章讨论,一起学习的朋友可以点点关注,会持续更新,文章有帮助的话可以长按点赞有惊喜收藏转发,有什么补充可以在下面评论,谢谢

相关推荐

RHEL8和CentOS8怎么重启网络

本文主要讲解如何重启RHEL8或者CentOS8网络以及如何解决RHEL8和CentOS8系统的网络管理服务报错,当我们安装好RHEL8或者CentOS8,重启启动网络时,会出现以下报错:...

Linux 内、外网双网卡路由配置

1.路由信息的影响Linux系统中如果有多张网卡的情况下,如果路由信息配置不正确,...

Linux——centos7修改网卡名

修改网卡名这个操作可能平时用不太上,可作为了解。修改网卡默认名从ens33改成eth01.首先修改网卡配置文件名(建议将原配置文件进行备份)...

CentOS7下修改网卡名称为ethX的操作方法

?Linux操作系统的网卡设备的传统命名方式是eth0、eth1、eth2等,而CentOS7提供了不同的命名规则,默认是基于固件、拓扑、位置信息来分配。这样做的优点是命名全自动的、可预知的...

Linux 网卡名称enss33修改为eth0

一、CentOS修改/etc/sysconfig/grub文件(修改前先备份)为GRUB_CMDLINE_LINUX变量增加2个参数(net.ifnames=0biosdevname=0),修改完成...

CentOS下双网卡绑定,实现带宽飞速

方式一1.新建/etc/sysconfig/network-scripts/ifcfg-bond0文件DEVICE=bond0IPADDR=191.3.60.1NETMASK=255.255.2...

linux 双网卡双网段设置路由转发

背景网络情况linux双网卡:网卡A(ens3)和网卡B(...

Linux-VMware设置网卡保持激活

Linux系统只有在激活网卡的状态下才能去连接网络,进行网络通讯。修改配置文件(永久激活网卡)...

VMware虚拟机三种网络模式

01.VMware虚拟机三种网络模式由于linux目前很热门,越来越多的人在学习linux,但是买一台服务放家里来学习,实在是很浪费。那么如何解决这个问题?虚拟机软件是很好的选择,常用的虚拟机软件有v...

Rocky Linux 9/CentOS Stream 9修改网卡配置/自动修改主机名(实操)

推荐...

2023年最新版 linux克隆虚拟机 解决网卡uuid重复问题

问题描述1、克隆了虚拟机,两台虚拟机里面的ip以及网卡的uuid都是一样的2、ip好改,但是uuid如何改呢?解决问题1、每台主机应该保证网卡的UUID是唯一的,避免后面网络通信有问题...

Linux网卡的Vlan配置,你可能不了解的玩法

如果服务器上连的交换机端口已经预先设置了TRUNK,并允许特定的VLAN可以通过,那么服务器的网卡在配置时就必须指定所属的VLAN,否则就不通了,这种情形在虚拟化部署时较常见。例如在一个办公环境中,办...

Centos7 网卡绑定

1、切换到指定目录#备份网卡数据cd/etc/sysconfig/network-scriptscpifcfg-enp5s0f0ifcfg-enp5s0f0.bak...

Linux搭建nginx+keepalived 高可用(主备+双主模式)

一:keepalived简介反向代理及负载均衡参考:...

Linux下Route 路由指令使用详解

linuxroute命令用于显示和操作IP路由表。要实现两个不同子网之间的通信,需要一台连接两个网络的路由器,或者同时位于两个网络的网关来实现。在Linux系统中,设置路由通常是为了解决以下问题:该...

取消回复欢迎 发表评论: