【并发编程领域】从volatile关键字到CAS以及ABA问题
1.volatile是什么
volatile是java虚拟机提供的轻量级的同步机制
-
保证可见性 2.不保证原子性 3.禁止指令重排
2.JMM之可见性
JMM(java内存模型Java Memory Model 简称JMM)本身是一种抽象的概念并不真实存在,他描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
-
线程解锁前,必须把共享变量的值刷新回主内存。
-
线程加锁前,必须读取主内存的最新值到自己的工作内存。
-
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后在将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下
3.可见性代码实现
class MyData{
//每个线程都有一个自己主内存中值的一份拷贝,默认使用都是自己内存的值。
//描述的场景是:在main线程中创建一个线程,通过启动调用修改mydata.value值 然后看主内存线程是否可以读取到
//updateThread线程的值,如果不可以 说明线程之间的值是不可见的。
//加上volatile关键字 就可以看到volatile保证了线程之间的可见性。
volatile int value = 0;
public void setValue(){
value = 100;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(new Runnable() {
@Override
public void run() {
//线程加入
System.out.println(Thread.currentThread().getName()+"update value Thread join!");
try {
//线程睡眠3秒钟
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setValue();
System.out.println(Thread.currentThread().getName()+"update value success!");
}
},"update Thread").start();
while (myData.value == 0){
//空等待
}
System.out.println(Thread.currentThread().getName()+" game over!");
}
}
总结:通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的。
问题:这就可能存在一个线程AAA修改了共享变量X的值但还没写回到主内存时,另一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存共享变量X对线程B来说是不可见的,这种工作内存与主内存同步延迟线程就造成了可见性问题。
4.volatile不保证原子性
代码展示:
class MyData {
volatile int value = 0;
public void setValue() {
value = 100;
}
public void addPlus(){
value++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
//开启20个线程 i++操作
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
myData.addPlus();
}
}
}).start();
}
//如果当前线程大于2 说明除了主线程和垃圾回收线程还有别的线程在运行
//因此mian线程等待其他线程执行完毕之后 在执行main线程的代码
while (Thread.activeCount()>2){
Thread.yield();//主线程让步
}
System.out.println(Thread.currentThread().getName()+"\t"+myData.value);
}
为什么不保证原子性操作 ?
如何解决?
直接使用juc的atomic原子类 AtomicInteger atomicInteger = new AtomicInteger();//原子性操作
5.volatile禁止指令重排序
6.禁止指令重排
volatile实现禁止指令重排序优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier) 又称内存栅栏 是一个cpu指令,他的作用有两个
一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排序优化,如果在指令间插入一条Memory Barrier 则会告诉编译器和cpu 不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令重排序优化。内存屏障另一个作用是强制刷出各种CPU的缓存数据,因此任何cpu上的线程都能读取到这些数据的最新版本
小结:
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchroinzed和volatile关键字解决,他们都可以使得一个线程修改后的变量立即对其他线程可见。
对于指令重排序导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
7.单例模式问题
1.在一般的单例模式下 单线程环境没有问题 但是在多线程环境下 会出现多次调用初始化构造函数。因此为了完成 可以使用双端检测机制,但是又由于指令重排序问题的存在 必须加上volatile 虽然使用syn可以解决 但是syn是重量级锁。
public class SingleDemo {
private static volatile SingleDemo instance = null;
private SingleDemo(){
System.out.println(Thread.currentThread().getName()+" new instance!");
}
//dcl 双端检查
public static SingleDemo getInstance(){
if (instance == null){
synchronized (SingleDemo.class){
if (instance == null){
instance = new SingleDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//在单线程环境下 可以保证创建单例对象被唯一的初始化
// System.out.println(SingleDemo.getInstance() == SingleDemo.getInstance());
// System.out.println(SingleDemo.getInstance() == SingleDemo.getInstance());
// System.out.println(SingleDemo.getInstance() == SingleDemo.getInstance());
//多线程环境下 初始化构造方法可能被初始化了不止一次 使用双端检查
//因此 就出现了问题
// 0 new instance!
//1 new instance!
for (int i = 0; i < 10 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
SingleDemo.getInstance();
}
},i+"").start();
}
}
}
但是指令重排序只会保证串行语义的执行的一致性(单线程) 但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已经初始化完成,也就造成了线程安全问题。
8.CAS是什么
独占锁:是一种悲观锁,synchronized就是一种独占锁,会导致其他需要锁的线程挂起,等待持有锁的线程释放锁资源。
乐观锁:每次不加锁,假设没有冲突去完成操作,失败了就一直重试,知道成功。
1、CAS
乐观锁就是使用CAS这种机制(Compare And Set)
CAS中有三个数 一个内存中的值,一个预期的值,一个要修改的值,比较内存中的值和预期的值是否相等 如果相等说明在某一时间内没有其他线程修改这个值,说明数据一致性,因此,就进行更新操作。
2、非阻塞算法
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
比较并交换 根据当前版本号相同就修改成功 不相同就修改失败。
AtomicInteger atomicInteger = new AtomicInteger(20);
System.out.println(atomicInteger.compareAndSet(20, 200)+"\t"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(20, 100)+"\t"+atomicInteger.get());
9.CAS底层原理
10.CAS存在的问题
-
循环时间长开销大
-
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
但是 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3.会有ABA问题
11.ABA问题
CAS--->UnSafe------>CAS底层思想----->ABA------->原子引用更新----------》如果规避ABA问题
ABA的问题:假设当前有两个线程t1 t2
t1读取内存的值这个过程会存在一个间隙时间,当t1读取到value=100 但是这个过程中t2读取了该值value=100 并且修改为101 在修改成100 这个过程对于t1线程来说是透明的,因此当t1进行修改的时候 就CAS就可以成功。这就是ABA问题
12.ABA问题的解决
对于ABA问题的解决,我们可以使用带有版本号的AtomicStampedReference 来解决
https://www.cnblogs.com/549294286/p/3766717.html
package com.ncst.t1;
import javax.sound.midi.Soundbank;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author i
* @create 2019/12/27 16:59
* @Description 模拟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) {
System.out.println("ABA问题产生");
new Thread(new Runnable() {
@Override
public void run() {
atomicReference.compareAndSet(100,101);//A
atomicReference.compareAndSet(101,100);//B
System.out.println("t1线程执行成功!");
}
},"t1").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,200);
System.out.println("t2线程执行成功!");
}
}).start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.get());
System.out.println("ABA问题解决");
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = atomicStampedReference.getStamp();
//获取当前版本号标志值 进行修改
atomicStampedReference.compareAndSet(100,101,1,stamp+1);
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t AB修改成功 当前版本号:"+atomicStampedReference.getStamp());
}
},"t3").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = atomicStampedReference.getStamp();
System.out.println("t4获取版本号值为:"+stamp);
boolean flag = atomicStampedReference.compareAndSet(100,2019,1,atomicStampedReference.getStamp()+1);
System.out.println("修改成功与否:"+flag);
int stamp2 = atomicStampedReference.getStamp();
System.out.println("t4最后获取版本号值为:"+stamp2);
}
},"t4").start();
}
}
总结:通过volatile关键字 保证了可见性,禁止指令重排序,不保证原子操作。引入JMM内存模型中拷贝值与主内存之间数据同步的问题。进而在单例模式中,我们引入volatile关键字来解决。所以为了保证原子操作 我们引入了AtomicXXX相关类,AtomicXXX类底层的实现是通过CAS机制来保证的,CAS中主要是通过期望值来判断其他线程是否修改过,但是这是存在问题的。由此引入了ABA问题。ABA问题的解决时使用AtomicStampedReference来解决,通过时间戳机制保证。类似于版本号机制。