线程通信定义
线程通信的定义就是:当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。
线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。
所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程通信。
为什么需要线程通信?
多个线程并发执行时, 在默认情况下CPU是随机切换线程的。
虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信。
所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺,以此来帮我们达到多线程共同操作一份数据。
线程通信的方式
那么线程是如何通信的呢,大致有以下5种方式:
1.Volatile方式
Volatile有两大特性,一是可见性,二是有序性,其中可见性就是达到让线程之间进行通信的效果。
Volatile的内存模型图
Java内存模型规定了所有的变量都存储在主内存中,主内存可以理解为所有线程的共享变量。
然后,每条线程还有自己的工作内存,比如:线程A,线程B,这些线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝。
如果线程A要和线程B通信,则需要经过2个步骤:
- 1)线程A把本地内存A更新过的共享变量,刷新到主内存中;
- 2)线程B到内存中去读取线程A之前,已更新过的共享变量;
这保证了线程间的通信必须经过主内存,所以引入了Volatile来解决。
Volatile保证可见性原理图
如下所示:
当变量被Volatile修饰后,当某一个线程修改完该变量后,需要先将这个最新修改的值写回到主内存,从而保证下一个读取该变量的线程取得的就是主内存中该数据的最新值。
主内存和工作内存之间的交互有具体的交互协议,JMM定义了八种操作来完成。
这八种操作是原子的、不可再分的,它们分别是:lock,unlock,read,load,use,assign,store,write,其中lock,unlock,read,write作用于主内存;load,use,assign,store作用于工作内存。
这样就保证线程之间的透明性,也就达到间接线程通信的效果。
2.等待通知方式
等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。
调用过程会涉及如下几点:
- wait() 、notify()调用的前提都是获得了对象的锁,所以这里要配合 Synchronized获取锁使用;
- 调用 wait() 方法后线程会释放锁,进入 WAITING 状态,该线程也会被移动到等待队列中;
- 调用 notify() 方法会将等待队列中的线程移动到同步队列中,线程状态也会更新为 BLOCKED;
- 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。
等待通知有着一个经典范式,线程 A 作为消费者,线程 B 作为生产者。
线程 B 作为生产者
- 获取对象锁;
- 更改与线程 A 共用的判断条件;
- 调用 notify() 方法;
线程 A 作为消费者
- 获取对象的锁;
- 进入 while判断条件,并调用 wait() 方法;
- 当条件满足跳出循环执行具体处理逻辑;
伪代码如下:
//Thread A synchronized(Object){ while(条件){ Object.wait(); } //do something } //Thread B synchronized(Object){ 条件=false;//改变条件 Object.notify(); }
3.join方式
在很多应用场景中存在这样一种情况:主线程创建并启动子线程后,如果子线程要进行很耗时的计算。
那么主线程将比子线程先结束,但是主线程需要子线程的计算的结果来进行自己下一步的计算,这时主线程就需要等待子线程,java中提供可join()方法解决这个问题。
示例如下:
private static void join() throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { LOGGER.info("running"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }) ; Thread t2 = new Thread(new Runnable() { @Override public void run() { LOGGER.info("running2"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } }) ; t1.start(); t2.start(); //等待线程1终止 t1.join(); //等待线程2终止 t2.join(); LOGGER.info("main over"); }
在 t1.join() 时会一直阻塞到 t1 执行完毕,所以最终主线程会等待 t1 和 t2 线程执行完毕。
4.CountDownLatch 并发工具
CountDownLatch的作用很简单,就是一个或者一组线程在开始执行操作之前,必须要等到其他线程执行完才可以,可以实现 join 相同的功能,但是更加的灵活。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1,当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
如下图所示:
示例如下:
public class CountDownLatchDemo{ public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(5); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { final int no = i + 1; Runnable runnable = new Runnable() { @Override public void run() { try { Thread.sleep((long) (Math.random() * 10000)); System.out.println(no + "号运动员完成了比赛。"); } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); } } }; service.submit(runnable); } System.out.println("等待5个运动员都跑完....."); latch.await(); System.out.println("所有人都跑完了,比赛结束。"); } }
执行结果如下:
主线程等待所有运动员都跑完比赛后,直到 5 个运动员都完成了比赛之后,也就是说直到计数器为0,主线程才会继续。
4.CyclicBarrier 并发工具
CyclicBarrier 中文名叫做屏障或者是栅栏,也可以用于线程间通信。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier),它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门。
CyclicBarrier 基于 Condition 来实现的,在CyclicBarrier 类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。
如下图所示:
CyclicBarrier和CountDownLatch的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier 可以循环利用;
- CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值。
- CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待;
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程;
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》