java高并发编程系列四:线程间通讯

线程间通讯

线程间通讯与网络通信等进程间通讯方式不一样,线程间通讯又称为进程内通讯,多个线程间实现互斥访问共享资源时会相互发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等,以下通过案例来分析java提供原生的通信API及通信机制背后的原理。

一 同步阻塞和异步非阻塞

1.同步阻塞消息处理

加入一个系统功能,客户端提交Event至服务器,服务器接收到客户端请求之后,开辟线程处理客户请求,经过复杂业务计算,将结果返回给客户端,如图所示存在以下缺陷:

a 同步提交event,服务端接收event,处理event,染安后再业务处理,返回结果,客户端等待时间过长会陷入阻塞,导致二次提交耗时过长。

b 客户端提交的业务数不多,系统同时受理业务数量有限,也就是系统整体吞吐量不高。

c 一个线程处理一个event,会导致系统频繁的创建线程开启和销毁,增加了系统的额外开销。

d 在业务量达到峰值时,大量的业务线程阻塞,导致CPU频繁的切换,降低了系统的性能。

2.异步消息非阻塞处理,如果我们使用异步非阻塞的方式,则不仅可以提高系统的吞吐量,而且处理线程的数量也能控制在一个固定的范围,以增加系统的稳定性。

客户端提交Event以后,会立即得到返回的一个工单号,Event会加入到Event队列中,服务端有若干个工作线程,不短的从Event队列中获取任务并进行异步处理,最后将处理结果保存在一个结果集中。如果客户端想要获取处理结果,则可凭借工单再次查询。

异步处理的优势较明显,首先客户端不用等到结果处理结束后再返回,从而提高了系统的吞吐量和并发量。其次服务端的线程控制在一个可控范围是不会导致太多的CPU上下文切换的带来的资源消耗。但异步处理的方式也存在缺陷,比如客户端想要得到结果还需要再次调用查询。(在后面会讲解利用异步回调接口的方式解决)

2.单线程间通信

服务端有若干个线程从队列中获取相应的Event进行异步处理,这些线程如何知道队列里面有内容需要处理?比较笨的方法是就是轮询,如果有数据则读取数据并处理,如果没有则等待若干时间再次轮询,还有一张比较好的方式就是通知机制,如果队列中有Event,则通知工作线程开始工作,如果没有,线程等待。

a wait 和 notify

以下代码中Event中定义了一个队列,offer方法会提交一个Event至队列尾,如果队列已经满了,则调用的队列的wait方法,那么提交的线程将会阻塞。take方法会从头获取数据,如果队列中没有了数据,那么调用wait线程将会阻塞。notify方法会唤醒曾经执行monitor资源的wait方法进入阻塞的线程。以下是单个线程的通信。循环往复。

以上代码中,为了防止多线程对共享资源操作引起的数据不一致的问题,我们需要对共享资源进行同步处理,这里使用synchronized进行同步,如果队列已经达到了上限,那么会调用wait的方法,使线程进入阻塞加入 wait set 中,并且释放monitor锁。

b wait 和 notify并不是Thread线程特有的方法,而是Object中的方法,也就是JDK中每一个类都有这两个方法,以下是wait 的重载方法

$ wait的这3个重载方法豆浆调用wait(long timgout )这个方法,wait()等价于wait(0),表示永不超时。

$ wait (long timeout)方法导致线程进入阻塞,直到有其他线程调用了object的notif或者notifyAll方法,或者阻塞时间达到了timeout而自动唤醒。

$ wait 必须拥有该对象的monitor锁,也就是wait方法必须在同步方法中使用

$ 当线程执行了该对象的wait方法以后,将会释放这个对象的monitor的锁的执行权,并进入该对象的 wait set中进入阻塞,其他线程也会有机会继续争抢该monitor的所有权。

以下是notify 的方法;

public final native void notify();

$ 唤醒正在执行该对象wait方法的线程,

$ 如果某个线程正在执行该对象的wait方法而进入阻塞,则会被唤醒,否则将会被忽略。

$ 被唤醒的资源徐亚重新获取该对象的关联的momitor锁才会继续执行。

c wait 和 notify使用注意事项

1 wait方法是可中断方法,说明,当前线程一旦调用了wait方法进入阻塞状态,其他线程可以使用interrupt将其打断,可中断方法被

别打断后会收到中断异常InterruptException异常,同时interrupt标识也会擦除。

2 线程执行了某个对象的wait方法以后,会加入与之对应的wait set中,每一个对象的monitor都有与之关联的wait set 中

3 当线程进入 wait set 中notify可以将其唤醒,也就是从wait set 中弹出,同时中断wait中的线程也将会被唤醒。

4 必须在同步方法中使用wait 和 notify方法,因为这两个方法执行的前提条件是必须持有同步方法的monitor的所有权,否则将会抛出非法的monitor状态异常。

5.同步代码中的monitor必须与执行的wait 和 notify的方法的对象一致,简单的说用哪个对象的monitor进行同步,就只能用哪个对象进行wait和notify操作,否则都会抛出moitor状态异常。

同步方法的monitor是this对象。虽然是在同步方法中执行wait和notify,但是该方法的执行以获取this的monitor锁以为前提。

d wait和sleep的区别之处;

1.wait和sleep都可以使线程进入阻塞状态,

2.都是可中断方法,被中断后都会抛出可中断异常

3 wait是对象的持有的方法,而sleep是Thread特有的方法。

4.wait方法的执行必须在同步方法中执行,而sleep职责不需要

5 当他们都在同方法中执行=时,sleep并不会释放掉monitor 的锁,而wait会释放掉monitor 的锁

6 sleep正在休眠时间以后,会主动退出阻塞,而wait方法在没有wait时间则需要别的线程中断以后在退出阻塞。

3 多线程通信

以上两个线程间的通信,一个线程offer提交事件,一个线程take处理事件,如果多个线程同时offer和take就会出现问题。

a 生产消费者

1)对象的notifyAll,这个方法与notify类似,都可以唤醒调用wait方法而阻塞的线程,但notify只能唤醒其中一个,而notifyall可以同时唤醒全部的阻塞线程,同样被唤醒的线程需要继续争抢的monitor 的锁。

2)生产消费者

以上的eventQueue队列在多线程并发的情况下会出现数据不一致的情况。增加测试线程,会出现队列没有元素仍然调用addFirst,在队列满的时候仍然添加addLast。因为wait方法会释放掉对象的monitor锁,所以多线程可能数据不一致。

改进以上代码:将临界值的判断if更改为while,将notify修改为notifyAll,线程自行争抢monnitor锁。

b 线程休息室wait set

在虚拟机规范中存在一个wait set,又称为线程休息室,它的数据结构并没有给出明确的定义,但是线程在调用了对象的wait的方法之后都会被加入到与该对象monitor关联的wait set中,并且释放对象的monitor的所有权。

等待另外一个线程调用对象的notify方法之后,其中一个线程会从wait set中弹出,至于随即弹出还是先进先出的方式弹出,虚拟机规范同样没有明确说明。

而执行notify不用考虑哪个线程会弹出,与该对象monitor关联的wait set中的线程全部会弹出。唤醒正在wait set中休息的所有线程。

4 自定义显式锁BooleanLock

自定义显式锁类似于java util下的Lock接口,并且分析synchronized关键字缺陷:

a synchromized提供了一种排他式的数据同步机制,某个线程在获取monitor lock锁的时候可能会被阻塞,这种阻塞有很明显的缺陷,第一无法控制阻塞时长,第二阻塞不可以被中断。

以上有一个同步的方法synchronized,启动了两个线程去调用它,首先T1线程会先进入同步,T2线程启动执行方法时会陷入阻塞,T2什么时候能获得方法的执行权完全由T1什么时候释放执行权。如果T2需要一定时间未获得无法放弃等待,就是阻塞时长无法控制。

第二个缺陷就是T2因争抢某个monitor锁而进入阻塞状态,那么它是无法中断的,虽然可以设置T2的中断标识,但是无法捕获到中断信号。证明了synchronized同步的线程不可被中断。

b 显式锁 BooleanLock 使其具备synchronized关键字所有的功能,同时又具备可中断和超时的功能。

通过控制台显示,每次只有一个线程能够获得锁的执行权限,它和synchronized非常类似。可以实现控制超时时间。

它也是可中断阻塞的线程,可以捕获到中断异常。

改进 :中断以后删除该线程。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181211G1H3IO00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券