Java并发-JMM、volitile、CAS
主内存与工作内存
JMM的目标是规定各个变量的访问规则,JMM规定所有变量存储再主内存,每条线程有自己的工作内存,线程工作内存保留着主内存的副本,线程对变量的所有操作都只能在工作内存中进行,不能操作主内存,也不能访问其他线程的变量。
Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。
JMM定义了8种原子操作保证及一些限定条件保证了一些线程安全的内存访问操作。直接叙述太抽象,上图!
图片来自:https://blog.csdn.net/yehongzhi1994/article/details/108700230
lock,作用于主内存中的变量,把变量标识为线程独占的状态。
read,作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
load,作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
use,作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign,作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
store,作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
write:作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
JMM又定义了一些规则来管理这8条操作,达成并发安全的目的。
volatile关键字
volatile是JVM提供的最轻量级的同步机制。
被volitile修饰的变量具备两种特性
- 可见性:当一个线程修改了这个变量,对其他线程是立即可见的
一个volatile变量被修改了以后,写回主内存,同时这个变量在其他线程的工作内存中对应的缓存行失效,进行read操作时要去从主内存查找。 - 禁止指令重排序
指令重排序具有数据依赖性,在单线程下不会影响原来的结果。但是多线程条件下,由没有了语句1和2没有数据依赖性,就有可能出现重排序,这时程序就又可能出错。
volitile通过插入内存屏障的方式防止指令重排。
volatile使用场景
//线程1:
//线程1
volatile boolean shutdownRequested;
...
public void shutdown() {
shutdownRequested = true;
System.out.println("关闭");
}
public void doWork() {
System.out.println("开始");
while (!shutdownRequested) {
}
}
如果线程A在运行doWork()方法,线程B想通过doWork来结束线程1,那么必须要用到volitail关键字,这就是vilitile可见性的一种体现。
接下来看一看volatile的第二个特性–禁止指令重排序。
package concurrent;
public class Singleton {
private volatile static Singleton instanse;
public static Singleton getInstanse() {
if (instanse == null) {
synchronized (Singleton.class) {
if (instanse == null) {
instanse = new Singleton();
}
}
}
return instanse;
}
}
在这个单例模式中,如果instance对象没有用valitile修饰,回发生如下问题。
JVM在实例化对象的时候分为以下三个步骤
memory = allocate(); //1: 分配内存空间
ctorInstance(memory); //2: 初始化对象
instance = memory; //3: 将内存空间的地址赋值给对象的引用
但是由于重排序的原因,2,3没有数据依赖,可能会进行重排序。
看着挺有道理的,但是实际上我没有测试出来,求指点。
CAS
CAS全称Compare and swap,实现乐观锁的一种方式,主要涉及三个操作数
A: 进行比较的值
B: 拟写入的新值
V: 需要读写的内存值
当V地址的值等于A时,赋值B,否则自旋直至V地址的值为A。
java的Unsafe类实现了CAS操作,通过调用硬件的原子操作实现,除此之外,Unsafe还实现了一些方法,比如获取对象熟悉的偏移地址,一些原子类的修改、更新操作就是基于此实现的。
CAS作为一种轻量级的同步机制,也存在一些缺点,例如:
1、ABA问题, 可以引入版本号来解决,JDK1.5之后,新增AtomicStampedReference类来处理这种情况
2、长时间自旋会给CPU带来负担,降低程序运行效率