Java多线程通讯

什么是多线程通讯?

多线程之间通讯,其实就是多个线程在操作同一个资源,互相可以感知到其它线程的状态或行为。

线程通讯主要可以分为三种类型,分别为共享内存消息传递管道流。每种类型有不同的方法来实现。

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。

    volatile共享内存

  • 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。

    wait/notify等待通知方式
    join方式

  • 管道流

    管道输入/输出流的形式

    消息队列

多线程之间的通讯需求

有些复杂程序或者是系统需要多个进程或者线程共同完成某个具体的任务,那么也就需要进程之间通信和数据访问。比如会遇到需要所有子线程执行完毕通知主线程处理某些逻辑的场景,或者是线程 A 在执行到某个条件需要通知或者等待线程 B 执行某个操作等情况。

线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。

线程是操作系统中独立的个体,但是这些个体如果不经过特殊处理就不能成为一个整体,线程间通信就成为整体的必用方式之一。

多线程通讯模型

在这里插入图片描述

关于JMM的详细解析请参考这篇博文:一文看懂Java内存模型(JMM)

多线程之间如何实现通讯

系统要实现某个全局功能必定要需要各个子模块之间的协调和配合,就像一个团队要完成某项任务的时候需要团队各个成员之间密切配合一样。而对于系统中的各个子线程来说,如果要完成一个系统功能,同样需要各个线程的配合,这样就少不了线程之间的通信与协作。常见的线程之间通信方式有如下几种:

  1. 通过共享资源进行忙等待(Busy Wait)
  2. wait和notify/notifyAll
  3. await和signal/signalAll
  4. sleep/yield/join
  5. CyclicBarrier 栅栏
  6. CountDownLatch 闭锁
  7. Semaphore 信号量
  8. Socket套接字进行网络通信

1、通过共享资源进行忙等待(Busy Wait)

线程间发送信号的一个简单方式是直接在共享内存中操作对象。线程A在一个同步块里设置boolean型成员变量flag为true,线程B也在同步块里读取flag这个成员变量。

准备处理数据的线程B正在等待数据变为可用。换句话说,它在等待线程A的一个信号,这个信号使getFlag()返回true。线程B运行在一个循环里,以等待这个信号。

2、wait和notify/notifyAll

wait和notify/notifyAll是Object的方法,任何一个对象都具有该方法。在使用的时候,首先需要设置一个全局锁对象,通过对该锁的释放和持有来控制该线程的运行和等待。因此在调用wait和notify的时候,该线程必须要已经持有该锁,然后才可调用,否则将会抛出IllegalMonitorStateException异常。
确定要让哪个线程等待?让哪个线程等待就在哪个线程中调用锁对象的wait方法。调用wait等待的是当前线程,而不是被调用线程,并不是theread.wait()就可以让thread等待,而是让当前线程(实际执行wait方法的线程,而不是调用者的那个线程对象)进行等待。尽量不要把线程对象当做全局锁使用,以免混淆等待线程。

在这里插入图片描述

3、await和signal/signalAll

await和signal是Condition的两个方法,其作用和wait和notify一样,目的都是让线程挂起等待,不同的是,这两种方法是属于Condition的两个方法,而Condition对象是由ReentrantLock调用newCondition()方法得到的。Condition对象就相当于前面所说的中介,在线程中调用contiton.await()和condition.signal()可以分别使线程等待和唤醒。

4、sleep/yield/join

对于sleep()方法应该很熟悉了,让当前线程睡眠一段时间。期间不会释放任何持有的锁

对于yield()方法可能使用的情况少一下。其作用主要是让当前线程从运行状态转变为就绪状态,由线程调度重新选择就绪状态的线程分配CPU资源。至于最终会选取哪个线程分配CPU资源就由调度策略来决定了,有可能还是该线程,有可能换为其它线程。

对于join方法,作用是暂停当前线程,等待被调用线程指向结束之后再继续执行。

使用join的时候需要注意:

1、调用join的时候,当前线程不会释放掉锁,如果调用线程也需要该锁则就会导致死锁!

2、join方法不会启动调用线程,所以,在调用join之前,该调用线程必须已经start启动,否则不会达到想要的效果。

join的底层实际是就是使用了一个自旋等待机制,判断调用线程是否死亡,如果没有则一直让当前线程wait。可以看一下底层实现源码:

5、CyclicBarrier栅栏

CyclicBarrier字面理解为线程屏障,当指定数量的线程执行到指定位置的时候,才能触发后续动作的进行。其最终目的是让所有线程同时开始后续的工作。

例如:三个员工来公司开会,由于三人住的地方与公司距离不同,所以到会议室的时间也不同。而会议开始必须等待三者都到达会议室之后才能进行。

6、CountDownLatch闭锁

与CycliBarrier不同的是CountDownLatch是某一个线程等待其他线程执行到某一位置之后,该线程(调用countDownLatch.await();等待的线程)才会继续后续工作。而CycliBarrier是各个线程执行到某位置之后,然后所有线程一齐开始后续的工作。相同的是两者都属于线程计数器。

使用示例如下: boss等待所有员工来开会,当所有人员都到齐之后,boss宣布开始会议!!!

7、Semaphore 信号量

Semaphore在线程协作方面主要用于控制同时访问临界区资源的线程个数。信号量是属于操作系统层面的概念,jdk提供了操作接口。

根据结果可以看出只有当有线程释放资源之后,才会有新的线程获取到资源。即控制了同一时间访问临界区资源的线程数量。当Semaphore(1)设置为1的时候,此时可以当做锁来使用。

8、Socket套接字

利用Java的socket套接字的方式来实现,也就是常用的tcp和udp这些协议的封装,这种一般用于分布式系统,单机环境下线程通迅用这个就有点浪费和复杂了。

多线程通信带来的问题

多个线程同一时刻对同一份资源进行操作时,如果跟我们预期的结果不一样,就会产生线程不安全的问题。有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
同时线程安全的问题只会出现在多线程环境中。
举个例子:现在有两个线程分别是线程1和线程2,他们同时对一个共享变量count直接进行多次count++的操作,我们知道cout++的操作是分为三个步骤的:

  1. 读:线程读取count的值到自己的工作内存
  2. 改:线程在工作内存中对count进行加一的操作
  3. 写:线程将计算结果写入共享变量所在内存

在这里插入图片描述
上图一看并没有什么问题,但如果在线程1进行读后,假如他现在看到的count值为6,此时线程2也开始读,它看到的也是6,随后线程2继续进行改和写的操作,此时共享内存中的count值变为了7,但是线程1还是在对6这个值进行操作,这就产生了线程安全的问题。
在这里插入图片描述

多线程通信导致线程不安全的原因

  • 线程对共享变量的所有操作都在自己的工作内存中进行,不是直接从主内存中读写
  • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递主要通过主内存来完成
  • 多线程下的代码执行顺序是不确定的

怎样解决线程不安全

对多线程下共享的资源加锁。使用 synchronized 包围对应代码块,保证多线程之间是互斥的。每次只能有一个线程访问其共享资源。如多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,而且其他的变量的值也和预期的是一样的,不存在执行程序时出现意外结果。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值