JMM规定:多线程共享的变量存储在主存中,各线程拥有自己的工作内存,不能访问其他线程的工作内存。线程的工作内存存储主存中变量的副本。线程要操作主存中的共享变量,通过操作工作内存中的副本来实现,完成之后再同步回主存 。
并发编程中的三个问题:
- 原子性:操作是否完整一次执行完(中间是否会被打断)。
- 可见性:当多个线程访问同一个变量时,某一个线程修改变量值,其他线程能否立即看到该修改。在硬件层面,由于CPU高速缓存的使用,使得通常情况下的可见性不能得到保证。在JVM中,根据上述JMM规定,采用的线程工作内存和CPU高速缓存很相似,因此通常也不能得到保证。需要额外的方法,如锁和volatile关键字。
- 有序性:通常最终cpu执行的指令和代码顺序有较大的差别,有两个方面的原因:编译器的优化和CPU指令执行上的优化,导致乱序。在单线程中通常不会造成问题,但是多线程环境可能有问题。
在java中,对于上述三性的支持:
1. 只能保证对基本数据类型(除了long和double)的基本读取和赋值操作是原子的,其他类型不能保证原子性,需要synchronized和Lock来实现更大范围内的原子性。
2. 对可见性,volatile关键字提供支持,当对volatile关键字修饰变量进行修改时会将更新值刷新到主存,从而保证可见性。另外,通过synchronized和Lock方式当然可以保证可见性。
3. 有序性,volatile关键字保证了一定程度的“有序性”,使用synchronized和Lock保证多线程对同步代码块的序列化访问,当然保证了有序性。另外,JMM天然具备所谓的happens-before原则。包括:按照书写代码顺序执行原则-保证了在单线程下执行的正确性,即使在有指令优化的前提下;锁定规则-unlock先于lock操作;volatile关键字规则-volatile变量的写操作先于读操作;传递规则;etc.
volatile关键字
有两层语义:
- 当有线程修改volatile修饰的关键字时,该值会立即被更新到主存(保证可见性);
- 一定程度上禁止指令重排。
这里的一定程度上禁止指令重排有两层含义:
1. 执行到volatile变量的读取或修改处时,前面的代码一定已经全部执行完毕,后面的部分一定还没有开始执行。
2. 指令重排时保证不会将volatile变量的读写的前面部分和后面的部分发生重排,但是前面部分之间的指令可以重排,后面的部分也是。
volatile的实现机制:
- 在对应的部分插入了lock指令,作为指令隔离墙,保证两边的指令不会交错排;
- 并强制变量更新到主存;
- 并使其他线程对应变量的缓存失效。
使用场景:
1. 状态标记 flag
2. 著名的double check模式,单例模式的实现中,在有volatile关键字的支持时,double check模式可以实现单例模式在并发环境中的线程安全。