目录
1、通信的方式:
1.1、文件共享
1.2、变量共享
1.3、线程协作-JDK API
1.3.1suspend/remuse
1.3.2 wait/notify机制
1.3.3 park/unpark机制
2、总结
3、伪唤醒
4、结语
要想实现多个线程之间的协同,如:线程的执行顺序、获取某个线程的执行结果等。
涉及到线程之间的通信,分为下面四类:
①文件共享
②网络共享
③共享变量
④JDK提供的线程协调API:suspend/resume、wait/notify、park/unpark
线程1写数据到文件里,线程2读取文件中的数据内容,实现数据的交换
代码示例:
import java.nio.file.Files; import java.nio.file.Paths; public class Text { //共享文件 public static String filePath= "text.txt"; public static void main(String[] args) throws Exception { //线程1写入数据 new Thread(()->{ try{ while(true){ Files.write(Paths.get(filePath),("当前时间" + String.valueOf(System.currentTimeMillis())).getBytes()); Thread.sleep(1000L); } }catch (Exception e){ e.printStackTrace(); } }).start(); //线程2,读数据 new Thread(() ->{ try{ while(true){ Thread.sleep(1000L); byte[] allBytes = Files.readAllBytes(Paths.get(filePath)); System.out.println(new String(allBytes)); } }catch (Exception e){ e.printStackTrace(); } }).start(); } }
线程1写数据到内存里(某个变量),线程2读取内存中(某个变量)的数据内容,实现数据的交换
代码示例:
public class Text { //共享变量 public static String content = ""; public static void main(String[] args) throws Exception { //线程1写入数据 new Thread(()->{ try{ while(true){ content = "当前时间" + String.valueOf(System.currentTimeMillis()); Thread.sleep(1000L); } }catch (Exception e){ e.printStackTrace(); } }).start(); //线程2,读数据 new Thread(() ->{ try{ while(true){ Thread.sleep(1000L); System.out.println(new String(content)); } }catch (Exception e){ e.printStackTrace(); } }).start(); } }
JDK中对于需要多线程协作完成某一任务场景,提供了对于API支持。
多线程协作的典型场景:生产者-消费者模型。(线程阻塞,线程唤醒)
示例1:线程1去买包子,没有包子,则不再执行。线程2生产包子,通知线程1继续执行。
场景:线程1买包子,包子店没有包子则等待。线程2生产包子,并通知线程1可以买包子了。
API-被弃用的suspend挂起目标线程,通过remuse可以恢复线程执行。
太容易产生死锁,所以被弃用
正常用法:
public class Text { //共享变量 public static Object baozidian = null; public static void main(String[] args) throws Exception { Thread consumerThread = new Thread(()->{ try{ if (baozidian == null){ System.out.println("暂时没有包子,进入等待..."); Thread.currentThread().suspend();//消费者卡在这,等待通知 } System.out.println("买到包子 回家!"); }catch (Exception e){ e.printStackTrace(); } }); consumerThread.start(); //主线程等待3秒,再生产包子。 让thread线程先执行 Thread.sleep(3000L); baozidian = new Object(); consumerThread.resume(); System.out.println("生产了包子,通知消费者可以购买!"); } }
结果:
suspend/remuse死锁写法:
第一种:
死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码
public class Text { //共享变量 public static Object baozidian = null; public static void main(String[] args) throws Exception { Thread consumerThread = new Thread(()->{ try{ if (baozidian == null){ System.out.println("暂时没有包子,进入等待..."); synchronized(Text.class){ //拿到锁, Thread.currentThread().suspend();//挂起阻塞,并没有释放锁 } } System.out.println("买到包子 回家!"); }catch (Exception e){ e.printStackTrace(); } }); consumerThread.start(); //主线程等待3秒,再生产包子。 让consumerThread线程先执行 Thread.sleep(3000L); baozidian = new Object(); synchronized(Text.class){ //consumerThread没有释放锁,拿不到锁。 consumerThread.resume();//拿不到锁 唤醒不了consumerThread。产生死锁 } System.out.println("生产了包子,通知消费者可以购买!"); } }
结果:
consumerThread 线程拿到锁,没有释放就挂起。主线程拿不到锁,不能唤醒consumerThread线程,从而 产生死锁
suspend/remuse死锁写法:
第二种:
remuse先通知唤醒线程consumerThread。suspend后面又使线程consumerThread挂起。得不到通知,导致永久挂起阻塞不能执行。
public class Text { //共享变量 public static Object baozidian = null; public static void main(String[] args) throws Exception { Thread consumerThread = new Thread(()->{ if (baozidian == null){ System.out.println("暂时没有包子,进入等待..."); try{ Thread.sleep(5000L); //模拟处理时间,等待5秒 }catch (Exception e){ e.printStackTrace(); } Thread.currentThread().suspend();//挂起阻塞 } System.out.println("买到包子 回家!"); }); consumerThread.start(); //主线程等待3秒,再生产包子。 让consumerThread线程先执行 Thread.sleep(3000L); baozidian = new Object(); consumerThread.resume();//通知 consumerThread 执行 System.out.println("生产了包子,通知消费者可以购买!"); } }
这个方法只能由同一对象锁的持有者线程调用,也就是写在同步块里,否则会抛出illegalmonitorStateException异常。
wait 导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。
推荐理由:wait可以自动解锁,但是对顺序执行有要求,需要先wait后notify
注意:
虽然会wait自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。
public class Text { public static Object baozidian = null; /** 正常的wait/notify */ public void waitNotifyTest() throws Exception { // 启动线程 new Thread(() -> { if (baozidian == null) { // 如果没包子,则进入等待 synchronized (this) { try { System.out.println("1、进入等待"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println("2、买到包子,回家"); }).start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); synchronized (this) { this.notifyAll(); System.out.println("3、通知消费者"); } } public static void main(String[] args) throws Exception { Text text = new Text(); text.waitNotifyTest();; } }
上述代码:先执行wait方法使线程等待,后执行notifyAll方法唤醒所有等待的线程。
导致程序永久等待的wait/notify
notify先通知唤醒,后面执行wait使线程等待。notify已经执行,不再唤醒,永远等待
但是wait释放锁,所以比较好
public class Text { public static Object baozidian = null; /** 会导致程序永久等待的wait/notify */ public void waitNotifyDeadLockTest() throws Exception { // 启动线程 new Thread(() -> { if (baozidian == null) { // 如果没包子,则进入等待 try { Thread.sleep(5000L); } catch (InterruptedException e1) { e1.printStackTrace(); } synchronized (this) { try { System.out.println("1、进入等待"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println("2、买到包子,回家"); }).start(); // 3秒之后,生产一个包子 Thread.sleep(3000L); baozidian = new Object(); synchronized (this) { this.notifyAll(); System.out.println("3、通知消费者"); } } public static void main(String[] args) throws Exception { Text text = new Text(); text.waitNotifyDeadLockTest(); } }
线程调用park则等待“”许可”,unpark方法为指定线程提供“许可(permit)”。
不要求park/unpark的调用顺序。
多次调用unpark之后,再调用park,线程会直接运行。
多次调用unpark,只能得到一次“许可”,不会叠加许可。再调用park获得“许可”,直接运行,消耗一次许可。再调用park,线程则进入等待状态。当unpark又提供一次许可。则线程继续执行一次。
正常的park/unpark
先LockSupport.park()将当前线程,也就是消费者线程挂起,3秒之后,主线程 LockSupport.unpark(consumerThread);指定消费者线程继续执行。
执行的先后顺序,不影响。
/** 正常的park/unpark */ public void parkUnparkTest() throws Exception { // 启动线程 Thread consumerThread = new Thread(() -> { if (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并不是基于监视器锁的方式实现的,jvm底层提供的另外一种线程挂起方式。
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用法:(被弃用)
对同步锁的使用有要求,不会释放锁,容易死锁。对顺序有要求,先调用suspend后resume。容易导致永久挂起。
wait/notify用法:
对同步锁没有要求,wait方法会自动释放锁。对顺序有要求,先调用wait后notify。容易导致永久挂起。
park/unpark用法:
对同步锁的使用有要求,不会释放锁,容易死锁。对顺序没有要求,park挂起消费者线程,unpak给消费者线程执行“许可”后,消费者线程继续执行。
虽然都有缺陷,但是都比已经弃用的suspend/resume要好。
警告!之前代码用if语句来判断,是否进入等待状态,是错误的。
官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
伪唤醒是指:线程并非是notify、notifyAll、unpark等api调用而唤醒的,是更底层的原因导致的。
在上面所有例子中,将if改为while,防止伪唤醒。
本章内容,设计很多JDK多线程开发工具类,它底层实现的原理。