通信的方式
-
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。
涉及到线程之间相互通信,分为下面四类:
- 文件共享(通过文件的形式实现数据共享)
- 网络共享
- 共享变量
jdk提供的线程协调API- 细分为:
suspend/resume、wait/notify、park/unpark
- 细分为:
线程协作 - JDK API
-
JDK中对于需要多线程协作完成某一任务的场景,提供了对应API支持。
-
多线程协作的典型场景是:生产者 - 消费者模式。(线程阻塞、线程唤醒)
-
示例:线程1去买包子,没有包子,则不再执行。线程-2生产出包子。通知线程-1继续执行。
-
API - 被弃用的suspend和resume
-
作用:调用suspend挂起目标线程,通过resume恢复线程执行。
-
被弃用的原因,就是因为容易出现死锁。
-
正常的suspend/resume:
/** 包子店 */ public static Object baozidian = null; /** 正常的suspend/resume */ public void suspendResumeTest() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { if (baozidian == null) { // 如果没包子,则进入等待 System.out.println("1、进入等待"); Thread.currentThread().suspend(); } System.out.println("2、买到包子,回家"); }); consumerThread.start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); consumerThread.resume(); System.out.println("3、通知消费者"); } -
suspend/resume死锁示例
-
在同步代码中使用
/** 死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码 */ public void suspendResumeDeadLockTest() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { if (baozidian == null) { // 如果没包子,则进入等待 System.out.println("1、进入等待"); // 当前线程拿到锁,然后挂起 synchronized (this) { Thread.currentThread().suspend(); } } System.out.println("2、买到包子,回家"); }); consumerThread.start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); // 争取到锁以后,再恢复consumerThread synchronized (this) { consumerThread.resume(); } System.out.println("3、通知消费者"); } -
suspend比resume后执行
/** 导致程序永久挂起的suspend/resume */ public void suspendResumeDeadLockTest2() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { if (baozidian == null) { System.out.println("1、没包子,进入等待"); try { // 为这个线程加上一点延时 Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } // 这里的挂起执行在resume后面 Thread.currentThread().suspend(); } System.out.println("2、买到包子,回家"); }); consumerThread.start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); consumerThread.resume(); System.out.println("3、通知消费者"); consumerThread.join(); }
-
-
-
wait/notify机制
-
这些方法只能由同一对象锁的持有者线程调用,也就是
写在同步块里面,否则回抛出IllegalMonitorStateException异常。 -
wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
-
虽然会wait自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于waiting状态。
-
-
park/unpark 机制
-
线程调用park则等待“许可”,unpark方法为指定线程提供“许可(permit)”。
-
不要求 park 和 unpark 方法的调用顺序。
-
多次调用 unpark 之后,再调用park,线程会直接运行。
-
但
不会叠加,也就是说,连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。 -
park/unpark 的使用我们需要借用 java.until.concurrent.locks.LockSupport 工具类提供的支持。
-
正常的park/unpark:
/** 正常的park/unpark */ public void parkUnparkTest() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { while (baozidian == null) { // 如果没包子,则进入等待 System.out.println("1、进入等待"); LockSupport.park(); } System.out.println("2、买到包子,回家"); }); consumerThread.start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); LockSupport.unpark(consumerThread); System.out.println("3、通知消费者"); } -
死锁的park/unpark:
/** 死锁的park/unpark */ public void parkUnparkDeadLockTest() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { if (baozidian == null) { // 如果没包子,则进入等待 System.out.println("1、进入等待"); // 当前线程拿到锁,然后挂起 synchronized (this) { LockSupport.park(); } } System.out.println("2、买到包子,回家"); }); consumerThread.start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); // 争取到锁以后,再恢复consumerThread synchronized (this) { LockSupport.unpark(consumerThread); } System.out.println("3、通知消费者"); }
-
-
总结:
- 弃用的suspend/resume:对调用顺序有要求,也要开发者在同步代码中自己注意锁的释放;
- wait/notify:要求在同步关键字里面使用,免去了死锁的困扰(会自己释放锁),但是对调用顺序由要求;
- park/unpark:没有要求调用的顺序,但是park并不会释放锁,所以在同步代码中使用要注意。
- 这一节的内容,设计很多
JDK多线程开发工具类,它底层实现的原理。掌握这些内容,后续课程中再进行回顾。
-
伪唤醒
