线程间通信

Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法(wait方法会释放线程所持有的锁)和 notify或者notifyAll方法来实现的。

那么java中线程的通信与同步是如何实现的呢?——Java内存模型(JMM)

JMM

JMM即Java Memory Model,它定义了主存,工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面

  1. 原子性-保证指令不会受到线程上下文切换的影响
  2. 可见性-保证指令不会受cpu缓存的影响
  3. 有序性-保证指令不会受cpu指令并行优化的影响

image.png

  • 线程访问共享变量(堆中数据)时,并不是直接访问,而是要经过JMM
  • 每个线程都保存了一份该线程需要使用到的共享变量副本,于是当线程A与线程B要通信,需要做以下步骤:
    • 线程a将本地内存的共享变量副本值更新到主内存
    • 线程b从主内存读取更新后的共享变量

总结: JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证


volatile关键字

volatile主要有以下两个功能:

  1. 保证变量的内存可见性(控制线程到主存中重新获取值)
  2. 保证有序性,禁止并行时指令重排序 (读写屏障)
  3. 注意! volatile并不保证原子性,也就是说,volatile不能阻止指令交错

我们来看看volatile 如何实现内存可见性

  1. public class demo8volatiledemo {
  2. int a = 0;
  3. volatile boolean flag = false;
  4. public void writer() {
  5. a = 1; // step 1
  6. flag = true; // step 2
  7. }
  8. public void reader() {
  9. if (flag) { // step 3
  10. System.out.println(a); // step 4
  11. }
  12. }
  13. }
  • 当运行step2时, JMM会立即把该线程对应的本地内存中的共享变量的值刷新到主内存
  • 当运行step3时, JMM会把立即该线程对应的本地内存置为⽆效,从主内存中读取共享变量的值。

在保证内存可⻅性这⼀点上,volatile有着与锁相同的内存语义,所以可以作为⼀个 “轻量级”的锁来使⽤。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,⽽锁可以保证整个临界区代码的执⾏具有原子性。 所以在功能上,锁比volatile更强⼤;在性能上,volatile更有优势 而且volatile更适合用在一个线程写,多个线程读的情景

happens-before

  • 对于程序员:JMM 向程序员提供的 happens-before 规则能满足程序员的需求,也向程序员 提供了足够强的内存可见性保证。
  • 对于处理器和编译器: 只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器 怎么优化都行。例如锁消除和volatile消除。

happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结。
image.png