线程间通信
Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法(wait方法会释放线程所持有的锁)和 notify或者notifyAll方法来实现的。
那么java中线程的通信与同步是如何实现的呢?——Java内存模型(JMM)
JMM
JMM即Java Memory Model,它定义了主存,工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
- 原子性-保证指令不会受到线程上下文切换的影响
- 可见性-保证指令不会受cpu缓存的影响
- 有序性-保证指令不会受cpu指令并行优化的影响
- 线程访问共享变量(堆中数据)时,并不是直接访问,而是要经过JMM
- 每个线程都保存了一份该线程需要使用到的共享变量副本,于是当线程A与线程B要通信,需要做以下步骤:
- 线程a将本地内存的共享变量副本值更新到主内存
- 线程b从主内存读取更新后的共享变量
总结: JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证
volatile关键字
volatile主要有以下两个功能:
- 保证变量的内存可见性(控制线程到主存中重新获取值)
- 保证有序性,禁止并行时指令重排序 (读写屏障)
- 注意! volatile并不保证原子性,也就是说,volatile不能阻止指令交错
我们来看看volatile 如何实现内存可见性
public class demo8volatiledemo {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // step 1
flag = true; // step 2
}
public void reader() {
if (flag) { // step 3
System.out.println(a); // step 4
}
}
}
- 当运行step2时, JMM会立即把该线程对应的本地内存中的共享变量的值刷新到主内存
- 当运行step3时, JMM会把立即该线程对应的本地内存置为⽆效,从主内存中读取共享变量的值。
在保证内存可⻅性这⼀点上,volatile有着与锁相同的内存语义,所以可以作为⼀个 “轻量级”的锁来使⽤。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,⽽锁可以保证整个临界区代码的执⾏具有原子性。 所以在功能上,锁比volatile更强⼤;在性能上,volatile更有优势 而且volatile更适合用在一个线程写,多个线程读的情景
happens-before
- 对于程序员:JMM 向程序员提供的 happens-before 规则能满足程序员的需求,也向程序员 提供了足够强的内存可见性保证。
- 对于处理器和编译器: 只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器 怎么优化都行。例如锁消除和volatile消除。
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结。