欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java并发深度总结:乐观锁CAS

程序员文章站 2022-07-10 09:02:02
执着于理想,纯粹于当下。内容1.悲观锁与乐观锁2.CAS的实现1.悲观锁与乐观锁悲观锁:悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动。获取共享资源的线程对共享资源加锁,其它线程被阻塞,其他只能等待锁被释放才可以执行。 Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。乐观锁:乐观锁和悲观锁对应,它认为数据的变动不会太频繁。因此,所以不会对共享资源上锁。线程在更新共享资源时,会去校验在此期间有没有其他线程去更新这个数据。 Java....

执着于理想,纯粹于当下。

1. 悲观锁与乐观锁

  • 悲观锁:悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动。获取共享资源的线程对共享资源加锁,其它线程被阻塞,其他只能等待锁被释放才可以执行。 Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

  • 乐观锁:乐观锁和悲观锁对应,它认为数据的变动不会太频繁。因此,所以不会对共享资源上锁。线程在更新共享资源时,才会去校验在此期间有没有其他线程去更新这个数据。 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

简单来说:

  • 悲观锁认为并发严重,每次都对共享资源加锁。
  • 乐观锁认为并发不那么严重,所以让线程不断去尝试更新。

2. CAS的实现

CAS(Campare and Swap),翻译过来就是 “ 比较并替换 ” ,是乐观锁的一种实现方式。

CAS操作包含三个操作操作数

  • 要更新的字段内存位置V
  • 旧的预期原值A
  • 要修改的新值B

CAS操作过程:

  1. 首先从内存位置V,读取预期原值A;
  2. 更新数据时,再次读取内存位置V的值,如果该值与预期原值A相匹配,将内存位置V的值更新为新值B;
  3. 若从内存位置V读取的值与预期原值A不同(其他线程在此期间对内存V处的值进行了修改),则从步骤(1)重试整个过程(自旋),直到修改成功。

Tip:在更新数据时,比较并更新是一个原子操作。
Java并发深度总结:乐观锁CAS

Java中的原子变量类都使用了CAS机制:

class CasMain {

    static AtomicInteger num = new AtomicInteger(0);

    public void add() {
        while (true) {
			// 读取原值A
            int prev = num.get();
			// 要修改的值 B
            int next = prev + 1;
			// 原子操作:比较并替换 false:CAS失败,自旋重试 ; true:CAS成功,结束循环
            if (num.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

// CAS 测试
public class CasTest{

    public static void main(String[] args) throws Exception{

        CasMain cas = new CasMain();
        ArrayList<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    cas.add();
                }
            }));
        }

        for (Thread t : threads) {
            t.start();
            t.join(); // 等待所有线程执行完毕
        }
        System.out.println(CasMain.num); // 输出:1000000
    }
}

注意:上述测试代码只是用于演示CAS修改共享资源的过程。CAS并不适用于线程竞争比较激烈的多线程环境。

3. CAS 实现原理

前面我们提到过,CAS中的 “比较并替换” 是一个原子操作,那么以AtomicInteger 为例,它的比较并替换,也就是compareAndSet()方法是怎么做到具有原子性的?

// AtomicInteger 的 compareAndSet 方法源代码:
 public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

可以看到 compareAndSet 方法是通过 Unsafe类的 compareAndSwapInt 方法实现的。

Unsafe类位于rt.jar包,该类提供了硬件级别的原子操作

Unsafe类中的方法能够通过直接操作字段偏移量来修改字段的值,相当于通过“指针”来操作字段,字段的偏移量需要自己计算,因此如果计算错误那么会带来一系列错误,比如会导致整个JVM实例崩溃等严重问题。因此Java并不鼓励使用该类,甚至命名为“Unsafe”。

Unsafe的compareAndSwapInt方法是一个native的方法,底层通过 C、C++调用操作系统的lock、cmpxchg 指令实现的

  • cmpxchg :比较并交换操作数。
  • lock:确保cmpxchg 是原子操作。

自己实现一个AtomicInteger:

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class MyAtomicInteger {

    private volatile int value;

    private static Unsafe unsafe;
    private static long valueOffset; // value字段偏移量

    static {
        try {
            // 反射获得Unsafe实例对象
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);

            //计算 value 字段 的 偏移量
            valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    // 减法
    public void decrement(int amount) {

        while (true) {
			// 原值 A
            int prev = getValue();
			// 要修改的值 B
            int next = prev - amount;
			// 比较并替换
            if (unsafe.compareAndSwapInt(this, valueOffset, prev, next)) {
                break;
            }
        }
    }
}

// 测试
class MyAtomicIntegerTest implements Runnable{

    private static MyAtomicInteger myAtomicInteger = new MyAtomicInteger(1000000);

    @Override
    public void run() {

        for (int i = 0; i < 1000; i++) {
            myAtomicInteger.decrement(1);
        }
    }

    public static void main(String[] args) throws Exception{

        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(new MyAtomicIntegerTest()));
        }
        for (Thread t : threads) {
            t.start();
            t.join();
        }
        System.out.println(myAtomicInteger.getValue()); // 0
    }
}

4. CAS的三大问题

4.1 ABA问题

CAS在更新数据时,会将内存位置V的值和原值A比较,只有这两个值是相同的,才会去更新数据。

ABA问题指的是:CAS更新数据时,只知道内存位置V的值有没有发生改变,在内存位置V的值和原值A相同的情况下,无法感知是否有其他线程将内存位置V的值修改过(其他线程可能在此期间,先把内存V的值从A改为B,再由B改为A的情况)

Java并发深度总结:乐观锁CAS
ABA问题的具体过程:

  1. 线程1读取了数据A;
  2. 线程2读取了数据A;
  3. 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B;
  4. 线程3读取了数据B;
  5. 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A;
  6. 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值;

可以发现,当出现ABA问题时,CAS操作依然是成功的。下面举一个实例,说明ABA在现实事件中引发的问题:

假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
Java并发深度总结:乐观锁CAS
由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
.
理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。
Java并发深度总结:乐观锁CAS
线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。
.
线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。
Java并发深度总结:乐观锁CAS
线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50。
Java并发深度总结:乐观锁CAS

因此,我们希望,只要在比较更新时,发现原值被修改过,就算这一次的CAS失败。具体的解决方案是:每次操作加版本号

也就是说,在比较更新时,不仅比较原值A,还要比较原值A的版本号是否和内存位置V的值的版本号是否相同

如:1A --> 2B --> 3A ;1A != 3A 更新失败。

Tip:

在Atomic包中,提供了一个现成的AtomicStampedReference类来解决ABA问题(除了对更新前的原值进行比较,也需要用更新前的 stamp标志位来进行比较)。

4.2 循环时间长开销大

由于线程并不会阻塞,如果CAS自旋长时间不成功,这会给CPU带来非常大的执行开销。因此CAS不适用于线程竞争激烈的多线程环境。

4.3 只能保证一个共享变量的原子操作

CAS操作单个共享变量的时候可以保证原子的操作,多个变量可以加锁 或者使用JDK 5之后的AtomicReference类,

AtomicReference类来保证引用对象之间的原子性,因此可以把多个变量放在一个对象里来进行CAS操作。

5. CAS 和 synchronized 的应用场景

  • CAS适用于读多写少的场景(冲突一般较少)

  • synchronized适用于写多读少的场景(冲突一般较多)

  1. 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换会导致用户态内核态间的切换,从而额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

  2. 对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

本文地址:https://blog.csdn.net/qq_42080839/article/details/110352073