CAS(Compare And Swap)乐观锁

小龙 939 2020-03-18

一、简介

什么是乐观锁,什么是悲观锁?

  • 悲观锁:线程每次去获取数据的时候都认为其他线程会对数据进行修改,所以每次操作都将资源上锁,其他的线程想要取到数据就会进入阻塞,直到该线程释放锁。读锁,写锁,数据库中的行锁,表锁都是悲观锁。
  • 乐观锁:线程每次获取数据的时候都认为不会有其他线程对资源进行修改,所以不上锁,但是在跟新的时候会判断一下在此期间有没有其他的线程对该资源进行了修改,如果修改了那么本次更新就会失败,反之则成功。版本号,CAS都是乐观锁机制。
    我们先设计一个程序来看一下并发问题
public class TestDemo {
public class TestDemo {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // 开10个线程
        for (int x = 0; x < 10; x++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //每个线程对count进行一万次加法
                    for (int y = 0; y < 10000; y++) {
                    	count++;
                    }
                }
            }).start();
        }
        Thread.sleep(2000);
        System.err.println(count);
    }
}

该代码的结果每次执行都会不一样,但是基本上都会比预计的数小很多。因为这个程序是线程不安全的(count++不是原子操作)。
我们在程序中加入“synchronized”同步锁

public class TestDemo {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // 开10个线程
        for (int x = 0; x < 10; x++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //每个线程对count进行一万次加法
                    for (int y = 0; y < 10000; y++) {
                        synchronized (TestDemo.class){
                            count++;
                        }
                    }
                }
            }).start();
        }
        Thread.sleep(2000);
        System.err.println(count);
    }
}

在使用了“synchronized”同步锁之后,count的自增操作变成了原子性操作,所以最终的输出结果一定是正确的,代码实现了线程安全。

使用synchronized虽然保证了线程安全,但是在性能上却不是最优的。synchronize属于悲观锁,无论资源存不存在竞争都要进行加锁,synchronize关键字会让没有的到锁的线程进入BLOCKED(阻塞)状态,而后在争夺到资源后恢复为RUNNABLE(运行)状态,这个过程中涉及到操作系统用户模式和内核模式的转换,随着并发量的增加,并且如果加锁的时间比较长,那么其性能开销将会变得很大。
在JDk1.6时对synchronized坐了一些优化,增加了偏向锁到轻量级锁再到重量级锁的过度。但是在转换为重量级锁的之后,性能依旧比较低。
可以使用基于冲突检查的乐观锁可以解决性能的问题,乐观锁模式下已经没有锁的概念了,每个线程都直接先去执行操作,操作完成之后检测是否和其他线程存在共享资源的竞争,如果没有就成功,如果存在共享资源的竞争则不短的重新执行操作,直到成功为止,重新尝试的过叫做自旋锁。自旋锁:在线程进入BLOCKES(阻塞状态)之前先自旋一段时间,在这期间可能线程已经释放锁了,从而避免了线程进入阻塞状态。

Atomic原子操作

所谓的原子操作类,指的就是java.util.concurrent.atomic包下,一些列以Atomic开头的包装类,例如:AtomicBoolean、AtomicInteger、AtomicLong。它们分别用于Boolean、Integer、Long类型的原子性操作。

public class TestDemo {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        // 开10个线程
        for (int x = 0; x < 10; x++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //每个线程对count进行一万次加法
                    for (int y = 0; y < 10000; y++) {
                        //自增
                        count.incrementAndGet();
                    }
                }
            }).start();
        }
        Thread.sleep(2000);
        System.err.println(count);
    }
}

使用了AtomicInteger之后,最终的结果依旧是正确的,并且在某些情况下性能比synchronized更好。
Atomic操作的底层实现正是利用的CAS机制

CAS机制

CAS是Compare And Swap的缩写(比较并替换)
CAS机制中使用了3个基本操作数:

  • 内存地址 V
  • 旧的预期值 A
  • 新的修改值 B

1、在内存地址V中,存储值为10的变量

CAS1.png

2、此时有一个线程1要将该内存地址中的值增加1,所以这时旧的预期值A=10,要修改的新值为B=11

CAS2png.png

3、但是在线程1未将修改后的内容提交,此时有一个线程2也对该值进行了修改并提交了

CAS3.png

CAS5.png

此时内存中的值变成了11,但是线程1旧的预期值还是10,如果此时线程1保存数据,就需要将内存值个旧的预期值进行比较
V = 11
线程1:A = 10
此时 V != A
修改失败

CAS6.png
从思想上来说,synchronized属于悲观锁,悲观锁总是很悲观的认证程序之中存在有严重并发情况,所以严防死守。CAS属于乐观锁,总是乐观的认为程序之中存在的并发并不那么严重,所有让线程不断的去尝试更新。
可以通过下面的程序验证

public class TestDemo implements Runnable {
    private static AtomicBoolean flag = new AtomicBoolean(true);
    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName() + "、flag = " + flag.get());
        // 比较并设置,比较给定的给定的预期值是否是旧的预期值,如果是就将其更新为给定的新值
        if (flag.compareAndSet(true,false)){  //更新成功
            System.err.println(Thread.currentThread().getName()+",flag = "+flag.get());
            try {
                // 执行到的线程模拟延迟5秒
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 重新设置为true
            flag.set(true);
        }else {  //更新失败,给定的预期值与旧的预期值不一致
            System.err.println("重试机制,当前线程:"+Thread.currentThread().getName()+"、flag = "+flag.get());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 递归调用,更清晰的观察自旋锁
            run();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TestDemo ts = new TestDemo();
        Thread thread1 = new Thread(ts);
        Thread thread2 = new Thread(ts);
        thread1.start();
        thread2.start();
    }
}

执行结果

当前线程:Thread-0、flag = true
当前线程:Thread-1、flag = true
Thread-1,flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = false
重试机制,当前线程:Thread-0、flag = false
当前线程:Thread-0、flag = true
Thread-0,flag = false

这个程序五无论怎么运行,两个线程都会执行if=true条件,而且还不会产生线程的脏读脏写情况。程序能做到这一点完全依靠一个方法:public final boolean compareAndSet(boolean expect, boolean update)
CAS7.png
compareAndSet()方法可以分解成

  • conpare()方法:比较
  • set()方法:设置

flag.compareAndSet(true,false)
conpare(true)返回true那么就设置flag为false,这个时候其他线程无论怎么走都无法走到都无法走到只有的得到共享内存为true是的程序隔离方法去。

CAS源码

private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
 public AtomicInteger(int initialValue) {
        value = initialValue;
    }

AtomicInteger的值保存在value中,value通volatile保证操作的可见性,通过一个静态代码块保证类在加载时valueOffset已经有值了
Unsafe是一个不安全的类,提供了一些底层的操作,我们是不能使用这个类的,valueOffset是AtomicInteger对象value成员变量在内存中的偏移量

自增操作

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
// 第一个参数为当前的这个对象,如count.getAndIncrement(),则这个参数为count这个对象
// 第二个参数为AtomicInteger对象value成员变量在内存中的偏移量
// 第三个参数为要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
	    // 调用底层方法获取得到value的值
            var5 = this.getIntVolatile(var1, var2);
	// 通过var1和var2得到底层值,var5位当前值,如果底层值=当前值,则将值设为var5+var4,并返回true,否则返回false
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

总结

并发较低的时候比较适合使用CAS,并发高还是使用synchronized比较合适
CAS缺点
1、只能保证对一个变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

2、长时间自旋会给CPU带来压力

上面有个方法可以观察到,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销

3、ABA问题

如果内存值V初次读取的值为A,并且在准备赋值的时候检查到它的值任然为A,那么就能说它的值没有被别的线程修改过了嘛?
答案是否定的,有可能它在这段时间被另一个线程修改为了B,然后又被修改为了A,那么此时CAS操作就会误认为这个值从来就没发生过改变。这个问题就被称为ABA问题。Java的JUC并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
所以在使用CAS前要考虑清除“ABA”问题是否会对项目产生影响,影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更加高效