乐观锁(CAS)与悲观锁详解

1.Java并发包(java.util.concurrent)的主要实现机制:

采用Volatile保证内存的可见性;

采用CAS(Compare And Swap)算法保证数据的原子性。

2.乐观锁(CAS算法)和悲观锁(synchronized):

Synchronized就是一种悲观锁,因为它总是假设最坏的情况,每次去拿数据都会认为别人会修该,所以每次都会加锁,效率较低。

乐观锁其实是一种思想,它具体有两个步骤:冲突检测和数据更新。所以它不会每次访问都进行加锁控制,只是在进行更新操作时才进行冲突检测和数据更新,适合用于读操作较多的程序,因此大大提高了吞吐量。其实现方式比较典型的就是CAS算法。当多个线程同时更新同一个变量时,只会有一个线程成功修改,其他的都会失败,更新失败的线程不会被挂起,而是被告知在这次竞争中失败了,并可以再次尝试。

3.CAS(Compare And Swap)原理:

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization),JUC性能因此大大提升。。

CAS算法首先涉及三个操作数:

内存值(V)—>首次读取内存中的值;

预估值(A)—>再一次读取的内存中的值;

更新值(B)—>新值;

每次在进行更新操作时,当且仅当V==A(内存值和预期值相等时),后 V=B(将内存值更新为B),否则不进行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

//模拟CAS算法:
public class TestCAS {
    
    
    public static void main(String[] args) {
    
    
        Cas cas = new Cas(10);
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                //10个线程都进行对数据的更新操作
                int expectedValue = cas.getValue();  //更新前获得预期值
                boolean b = cas.CompareTest(expectedValue, (int) Math.random());
                System.out.println(b);

            }).start();
        }
    }
}
class Cas{
    
    
    /*
    * 内存值;预期值;新值。
    * */

    int value;  //内存值

    public synchronized int getValue() {
    
    
        return value;
    }

    public Cas(int value) {
    
       //构造方法
        this.value = value;
    }

    //比较交换,并将内存值返回
    public int CompareSwap(int expectedValue,int newValue){
    
    
        int oldValue=value;  //获得内存值
        if(oldValue==expectedValue){
    
    
            value=newValue;
        }
        return oldValue;
    }

    //检测是否更新成功
    public boolean CompareTest(int expectedValue,int newValue){
    
    
        boolean flag=false;
        if(expectedValue==CompareSwap(expectedValue,newValue)){
    
    
            //判断预期值和内存值是否相等->更新是否成功
            flag=true;
        }
        return flag;
    }
}

4.乐观锁的缺点

4.1 ABA 问题是乐观锁一个常见的问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

解决方法1:在修改值的同时增加一个时间戳,只有当预期值和时间戳都相同时才进行修改。

解决方法2:JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

4.2 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

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

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

4.4 CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),
synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充:

Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,**竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。**在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS

猜你喜欢

转载自blog.csdn.net/jingli456/article/details/114606841