CAS & volatile

发布时间 2023-04-05 20:45:18作者: YIYUYI

1. CAS

1.1 问题

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁
锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

1.2 解决

CAS:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

1.3 CAS的目的

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

1.4 CAS的问题

(1)ABA问题:被监控的值变化:A->B->A,而再次成为A的时候,通常程序无法发现。
解决:加入版本控制:A1->B2->A3
(2)循环时间开销
解决:如果JVM能支持处理器提供的pause指令那么效率会有一定的提升
(3)只能保证一个共享变量的原子操作
解决:把多个变量合并一同操作:i=2 & j=a -> ij=2a

2. volatite

——内存锁定,同一时刻只有一个线程可以修改内存值

2.1 变量不可见性

——多线程下变量不可见性
在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值的情况。

public class Volatile_ {
    public static void main(String[] args) {
        MyThread_ thread_ = new MyThread_();
        thread_.start();
        while (true) {
            synchronized (thread_) {
                if (thread_.isFlag())
                    System.out.println("true+");
            }
        }
    }
}

class MyThread_ extends Thread {
    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        flag = true;
        System.out.println(flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

(1) JMM & 不可见原理

——Java Memory Model

image.png

主内存:各个线程的共享变量;工作内容:单个线程的共享变量副本。
JMM有以下规定:
● 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题
● 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本
● 线程对变是的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量
● 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成
过程:
● 在程序加载时,flag被加载到了主内存中
● 在创建线程时,有一份flag被拷贝到了其工作内存中
image.png
可见性问题的原因:
所有共享变量存在于主内存中,每个线程由自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。

(2)解决

——加锁 & volatile

synchronized (thread_) {
    if (thread_.isFlag())
        System.out.println("true+");
}

原理:某一个线程进入synchronized代码块前后,执行过程入如下:
a. 线程获得锁
b. 清空工作内存
c. 从主内存拷贝共享变量最新的值到工作内存成为副本
d. 执行代码
e. 将修改后的副本的值刷新回主内存中
f. 线程释放锁
private volatile boolean flag = false;
原理:
volatile对变量进行修改之后,会通知到其他线程所对应的变量失效,需要更新。
image.png
volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

2.2 volatile,不保证原子性

场景:有变量=0;创建100个线程,每个线程对变量做1000次+1操作,最终的结果<100000

public class Volatile_ {
    public static void main(String[] args) {
        Runnable target = new Thread_M();
        for (int i = 0; i < 1000; i++) {
            new Thread(target).start();
        }
    }
}

class Thread_M implements Runnable {
    private volatile int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count++;
            System.out.println(count);
        }
    }
}

原因
image.png
volatile只是保证了多线程下变量修改的可见性,而并不保证多线程对变量修改的安全性。

(1)解决一:加锁

加锁:count++这个操作并不具有原子性,加锁保证其原子性

@Override
public void run() {
    synchronized(ThreadM_.class) {
        // 将当前任务类的字节码文件作为锁对象
        for (int i = 0; i < 100; i++) {
            count++;
            System.out.println(count);
        }
    }
}

(2)方法二:原子类-CAS

iava从DK1.5开始提供了ava.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了-种用法简单,性能高效,线程安全地更新一个变量的方式。
image.png
image.png
:“本地方法”

2.5 volatile:禁止指令重排序

什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序.
原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:
● 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序:
● 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
● 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
简单来讲:JVM上层程序执行和JVM底层依赖机器指令等合作执行可以提高效率,在不改变结果的情况下,一般都会支持指令重排序以获得较好的性能。
image.png
问题:并发执行下,JVM虚拟机并不能保证指令重排序带来的安全性问题。
场景:在交叉赋值的情况下,由于CPU的指令重排序特性,可能出现变量赋值顺序颠倒的情况。
解决:使用volatile修饰变量禁止重排序。


volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。
如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
    volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。