线程间实现通信的几种方式
线程通信相关概述
线程间通信的模型有两种:共享内存和消息传递,下面介绍的都是围绕这两个来实现
提出问题
有两个线程A和B,B线程向一个集合里面依次添加元素“abc”字符串,一共添加10次,当添加到第五次的时候,希望线程A能够收到线程B的通知,然后B线程执行相关的业务操作
方式一:使用Object类的wait() 和 notify() 方法
1. Object类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。
2. A,B之间如何通信的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?
这里用到了Object类的 wait() 和 notify() 方法。
当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态
当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
这种方式的一个好处就是CPU的利用率提高了。但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。
3. wait()和notify()为什么要在synchronized代码块中?
因为必须要有一个竞争条件控制线程在什么条件下等待,什么条件下唤醒。而Synchronized同步关键字就可以实现这样一个互斥条件,也就是在通过共享变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之前的通信。
4. wait()会释放当前对象的锁,但不会释放所有锁
5. notify()唤醒线程后,不会立即释放锁对象,需要等到当前同步代码块都执行完后才能释放锁对象
public static void main(String[] args) { //定义一个锁对象 Object lock = new Object(); List<String> list = new ArrayList<>(); // 线程A Thread threadA = new Thread(() -> { synchronized (lock) { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A添加元素,此时list的size为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) lock.notify();//唤醒B线程 } } }); //线程B Thread threadB = new Thread(() -> { while (true) { synchronized (lock) { if (list.size() != 5) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程B收到通知,开始执行自己的业务..."); } } }); //需要先启动线程B threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //再启动线程A threadA.start(); }
方式二:Lock 接口中的 newContition() 方法返回 Condition 对象,Condition 类也可以实现等待/通知模式
1. await()和signal()为什么要用ReentrantLock.lock()锁住?
原因和上一个【wait()和notify()为什么要在synchronized代码块中?】一样, 因为必须要有一个竞争条件控制线程在什么条件下等待,什么条件下唤醒。而ReentrantLock.lock()就可以实现这样互斥条件,也就是在通过共享变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之前的通信。
2. condition的await是否会释放线程占有的reentrantLock
condition.await() 会把当前线程的node放入等待condition的链表中,然后释放当前持有的锁,在condition.sign()的时候,会从等待condition的链表中取出node,放入争抢锁资源的node链表中,让他们自己去争抢资源
3. signal()取出node放入争抢锁的node链表后,不会立即释放锁对象,需要等到当前锁代码段都执行完后才能释放锁对象
public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); List<String> list = new ArrayList<>(); //线程A Thread threadA = new Thread(() -> { lock.lock(); for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A添加元素,此时list的size为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) condition.signal(); } lock.unlock(); }); //线程B Thread threadB = new Thread(() -> { lock.lock(); if (list.size() != 5) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程B收到通知,开始执行自己的业务..."); lock.unlock(); }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); }
方法三:使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
public class TestSync { //定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知 static volatile boolean notice = false; public static void main(String[] args) { List<String> list = new ArrayList<>(); //线程A Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A添加元素,此时list的size为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) notice = true; } }); //线程B Thread threadB = new Thread(() -> { while (true) { if (notice) { System.out.println("线程B收到通知,开始执行自己的业务..."); break; } } }); //需要先启动线程B threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 再启动线程A threadA.start(); } }
方法四:基本 LockSupport 实现线程间的阻塞和唤醒
LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具。使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
1. LockSupport.unpark(threadB)后会立刻唤醒线程B,并不需要等待线程B执行完成。
public static void main(String[] args) { List<String> list = new ArrayList<>(); //线程B final Thread threadB = new Thread(() -> { if (list.size() != 5) { LockSupport.park(); } System.out.println("线程B收到通知,开始执行自己的业务..."); }); //线程A Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("线程A添加元素,此时list的size为:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) LockSupport.unpark(threadB); } }); threadA.start(); threadB.start(); }
方法五:使用JUC工具类 CountDownLatch
jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书写,
一、CountDownLatch(减法计数器,归零后解除阻塞)
基于AQS框架,相当于也是维护了一个线程间共享变量state。
通过await()阻塞线程,当countDown()调用次数等于CountDownLatch(1)中定义的count时,就会立即激活阻塞线程B(不需要等待线程A执行完毕),如下:老板进入会议室等待5个人全部到达会议室才会开会。所以这里有两个线程老板等待开会线程、员工到达会议室:
public class CountDownLatchTest { private static CountDownLatch countDownLatch = new CountDownLatch(5); /** * Boss线程,等待员工到达开会 */ static class BossThread extends Thread{ @Override public void run() { System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会..."); try { //Boss等待 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("所有人都已经到齐了,开会吧..."); } } //员工到达会议室 static class EmpleoyeeThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName() + ",到达会议室...."); //员工到达会议室 count - 1 countDownLatch.countDown(); } } public static void main(String[] args){ //Boss线程启动 new BossThread().start(); for(int i = 0 ; i < countDownLatch.getCount() ; i++){ new EmpleoyeeThread().start(); } } }
二、CyclicBarrer(加法计数器,达到设定值,解除阻塞,放行)
加方计数器,与CountDownLatch 想反,当满足某种条件时才执行后续步骤。CyclicBarrer未达到设置的数量时,cyclicBarrier.await() 一直会阻塞,达到后放行,如下,我们开会只有等所有的人到齐了才会开会:
public class CyclicBarrierTest { private static CyclicBarrier cyclicBarrier; static class CyclicBarrierThread extends Thread{ public void run() { System.out.println(Thread.currentThread().getName() + "到了"); //等待 try { cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args){ cyclicBarrier = new CyclicBarrier(5, new Runnable() { @Override public void run() { System.out.println("人到齐了,开会吧...."); } }); for(int i = 0 ; i < 5 ; i++){ new CyclicBarrierThread().start(); } } }
三、Semaphore(信号量计数器,一次执行通过指定线程,线程获取信后,信号量-1,归零时阻塞,释放后,信号量后+1,信号量>0时不阻塞。)
信号量计数器,一次只能是指定个数线程,其他线程等待,线程释放后,其他线程才可进入,常用场景限流,设置一次通行2个线程,6个线程,只有其他线程放弃控制权,后续线程才可获得,如下,已停车为示例:
public class SemaphoreTest { static class Parking{ //信号量 private Semaphore semaphore; Parking(int count){ semaphore = new Semaphore(count); } public void park(){ try { //获取信号量 semaphore.acquire(); long time = (long) (Math.random() * 10); System.out.println(Thread.currentThread().getName() + "进入停车场,停车" + time + "秒..." ); Thread.sleep(time); System.out.println(Thread.currentThread().getName() + "开出停车场..."); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } } } static class Car extends Thread { Parking parking ; Car(Parking parking){ this.parking = parking; } @Override public void run() { parking.park(); //进入停车场 } } public static void main(String[] args){ Parking parking = new Parking(3); for(int i = 0 ; i < 5 ; i++){ new Car(parking).start(); } } }