JUC基础总结01 - volatile & cas
一、可见性的代码验证说明
二、volatile指令重排案例
三、CAS及ABA问题
一、可见性的代码验证说明
1.JMM,Java内存模型,本身是一种抽象概念并不真实存在。它描述的是一组规范,通过这个规范定义了程序中各变量的访问方式,是为了解决多核CPU缓存数据不一致问题提出的。
2.JMM关于同步的规定:
1) 线程解锁前,必须把共享变量的值刷新回主内存
2) 线程加锁前,必须读取主内存中最新值到自己的工作内存
3) 加锁解锁是同一把锁
3.JVM运行程序的实体是线程,每个线程(对应多核CPU线程)创建时,JVM都会为其创建一个工作内存(栈空间),线程间的通信必须通过主内存完成
主内存和工作内存之间的交互有具体的交互协议。对于交互协议,JMM定义了8种操作完成,分别是:lock、unlock、read、load、use、assign、store、write,其中:lock、unlock、read、write作用于主内存;load、use、assign、store作用于工作内存
1) 从主内存复制到当前工作内存:read and load
2) 执行代码,改变共享量值:use and assign
3) 用工作内存数据刷新主内存:store and write
4.volatile 可见性实践
// volatile 是JVM提供的轻量级同步机制:保证可见性、禁止指令重排、不保证原子性
public class VolatileDemo extends Thread {
public int number = 0;
// public volatile int number = 0;
@Override
public void run() {
System.out.println("线程开始...");
while (number == 0) {
// System.out.println("放开这个注释有意想不到的事发生...");
}
System.out.println("线程停止!");
}
public static void main(String[] args) {
VolatileDemo thread = new VolatileDemo();
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
thread.number += 30;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main线程已修改共享变量为" + thread.number);
}
}
意想不到的是,System.out.println方法中使用了 synchronized,即使没有使用volatile关键字,while循环时,不断取到number最新值
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
二、volatile指令重排案例
1.volatile 实现可见性和禁止指令重排,是依靠内存栅栏(一条CPU指令)完成
2.单例模式volatile分析
public class SingleTon01 {
private static SingleTon01 singleTon01 = null;
private SingleTon01() { }
public static SingleTon01 getSingleTon01() {
if (singleTon01 == null) {
singleTon01 = new SingleTon01();
}
return singleTon01;
}
public static void main(String[] args) {
// System.out.println(SingleTon01.getSingleTon01());
// System.out.println(SingleTon01.getSingleTon01());
// System.out.println(SingleTon01.getSingleTon01());
// System.out.println(SingleTon01.getSingleTon01());
// 多线程出现多个实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(SingleTon01.getSingleTon01());
}, String.valueOf(i)).start();
}
}
}
1) 解决方法1:在 getSingleTon01 方法上加 synchronized 关键字修饰
2) 解决方法2:双重检测锁 DCL 版
public class SingleTon01 {
private static SingleTon01 singleTon01 = null;
private SingleTon01() { }
public static SingleTon01 getSingleTon01() {
if (singleTon01 == null) {
synchronized (SingleTon01.class) {
if (singleTon01 == null) {
singleTon01 = new SingleTon01();
}
}
}
return singleTon01;
}
}
- 双重检锁机制不一定线程安全,原因是存在指令重排
- 线程1在初始化(执行singleTon01 = new SingleTon01())时,分为三步,分配的内存空间、创建对象、引用指向内存空间,但是后两步不存在依赖关系,即可能发生指令重排
- 线程2调 getSingleTon01 方法判断第一个if时,发现引用指向分配内存空间(此时,singleTon01 != null,但是此时线程1仅仅是分配了内存空间 + 引用指向内存空间)
- 线程2有概率 return null; 出现线程安全问题
此时需要加上 volatile: private static volatile SingleTon01 singleTon01 = null; // 保证分配的内存空间、创建对象、引用指向内存空间按顺序执行,这样线程2进入第一个if,等线程1完成后,线程2到达第二个if,发现对象已实例化完成,此时返回线程1的实例化对象
3.以后单例 = 双重检锁机制 + volatile禁止指令重排
三、CAS及ABA问题
1.unsafe类 位于 rt.jar\sun\misc包下,是CAS的核心类,Unsafe类中的方法都是native方法,是为JAVA执行特定内存操作而提供操作底层系统资源方法集
2.AtomicInteger中,就是通过unsafe类获取对象的内存地址偏移量再实现比较和替换,volatile 保证了多线程之间内存可见性
3.CAS缺点:1)循环时间长,开销会很大;2)只能对单个共享变量进行原子操作;3)ABA问题
4.ABA问题的解决
public class AbaDemo {
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
// ABA问题 原型
// new Thread(() -> {
// System.out.println(Thread.currentThread().getName() + "\t begin");
// atomicReference.compareAndSet(100, 101);
// atomicReference.compareAndSet(101, 100);
// System.out.println(Thread.currentThread().getName() + "\t end");
// }, "t1").start();
// new Thread(() -> {
// System.out.println(Thread.currentThread().getName() + "\t begin");
// // 先休息 保证 t1线程 完成ABA操作
// try {
// TimeUnit.SECONDS.sleep(1);
// boolean res = atomicReference.compareAndSet(100, 101);
// System.out.println("操作结果:" + res + ", 当前值:" + atomicReference.get());
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName() + "\t end");
// }, "t2").start();
System.out.println("================================");
// ABA问题解决 使用AtomicStampedReference
new Thread(() -> {
int version1 = atomicStampedReference.getStamp();
System.out.println("t3拿到的初始版本:" + version1);
try {
// 休息1秒,让t4拿到相同版本的100
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, version1, version1 + 1);
int version2 = atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet(101, 100, version2, version2 + 1);
}, "t3").start();
new Thread(() -> {
// 先休息 保证 t1线程 完成ABA操作
try {
int version = atomicStampedReference.getStamp();
System.out.println("t4拿到的初始版本:" + version);
// 休息3秒,让T3完成ABA操作,t4再尝试getAndSet
TimeUnit.SECONDS.sleep(3);
boolean res = atomicStampedReference.compareAndSet(100, 101, version, version + 1);
System.out.println("t4操作结果:" + res + ", 当前值:" + atomicStampedReference.getReference() + ", 当前版本:" + atomicStampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start();
}
}