CAS机制是什么

CAS是一种乐观锁机制,先看一个例子,开启两个线程,循环10000次,每次让count值加1,如果线程是安全的那么得到的结果应该是20000。

在没有使用锁的情况下

public class CASExample extends Thread {
    public static int count = 0;

    @Override
    public void run() {
        //循环的次数得设置大一些

        for (int i=0; i<10000; i++) {           
                  count++;       
        }
    }
    public static void main(String[] args) throws InterruptedException {

        CASExample task1 = new CASExample();
        CASExample task2 = new CASExample();
        task1.start();
        task2.start();
        Thread.sleep(2*1000);
        System.out.println(count);

    }
}
输出为

12161

反复运行几次还是没有预期的值20000,这是因为很可能存在两个线程同时访问这个变量count值。

如果使用悲观锁 synchronized ,我们可以得到我们预期要的结果,但是悲观锁的效率没有乐观锁的效率高。

使用synchronized 悲观锁

import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

public class CASExample2 extends Thread {
    public static int count = 0;

    public static long getTimeStamp(){
        Date date = new Date();
        long timestamp = date.getTime();//返回13位时间戳
        return timestamp;
    }
    //public static AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        //循环的次数得设置大一些
        System.out.println();
        long time1 = getTimeStamp();
        for (int i=0; i<10000; i++) {
            synchronized(CASExample2.class){
                count++;
            }
            //count.incrementAndGet();

        }
        long time2 = getTimeStamp();
        System.out.println("线程 " + Thread.currentThread().getName() + ",时间差值 "+(time2-time1));
    }
    public static void main(String[] args) throws InterruptedException {

        CASExample2 task1 = new CASExample2();
        CASExample2 task2 = new CASExample2();
        task1.start();
        task2.start();
        Thread.sleep(2*1000);
        System.out.println(count);

    }
}

测试一下

输出

Java中的CAS机制,如何解决ABA问题

我们再来使用乐观锁 AtomicInteger

import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

public class CASExample2 extends Thread {
//    public static int count = 0;

    public static long getTimeStamp(){
        Date date = new Date();
        long timestamp = date.getTime();//返回13位时间戳
        return timestamp;
    }
    public static AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        //循环的次数得设置大一些
        System.out.println();
        long time1 = getTimeStamp();
        for (int i=0; i<10000; i++) {
//            synchronized(CASExample2.class){
//                count++;
//            }
            count.incrementAndGet();

        }
        long time2 = getTimeStamp();
        System.out.println("线程 " + Thread.currentThread().getName() + ",时间差值 "+(time2-time1));
    }
    public static void main(String[] args) throws InterruptedException {

        CASExample2 task1 = new CASExample2();
        CASExample2 task2 = new CASExample2();
        task1.start();
        task2.start();
        Thread.sleep(2*1000);
        System.out.println(count);

    }
}

测试一下

输出

Java中的CAS机制,如何解决ABA问题

显然乐观锁AtomicInteger的执行效率是优于悲观锁的synchronized。其中AtomicInteger用到了CAS机制,CAS全称是 Compare And Swap即比较并替换。CAS有3个操作数

  • 内存值V
  • 旧的预期值A
  • 要更改的新值B

CAS说的直白点就是在更新数据前,先判断内存中的值是否和预期的值是否相同,相同则更新,不同则等待,这个等待并重新尝试的过程则是自旋,直至修改完成。比如在上面的例子中

  1.  线程1想要执行count=count+1,此时线程1获取到的内存值V的旧的预期值A的值为100,想要更改的新值B为101,
  2.  在线程1提交前,线程2提前完成了地址V中所对应值的修改,此时地址V所对应的旧值A变为101了,
  3.  线程1使用CAS机制比较并替换原则,发现地址V中的旧的预期值A不再是100,提交失败。
  4.  线程1 重新获取地址V所对应的A值为101了,同时,B值也变为102,这个重复等待并尝试的过程则是自选。
  5.  此时线程1不再有其它的线程抢在它前面提交,终于提交成功,将count值修改为102。

显然,在高并发的时候,这个自旋的过程会导致cpu的开销较大。

 ABA问题

ABA的问题需要通过例子说明,我们还是用上面的例子

  1. 线程1想要执行操作,此时线程1获取到的A值是100,然后由于某种原因,线程1被挂起了。
  2. 线程2也执行操作,先将A的值改为B=101,随后又将B的值改为100,此时V所对应的数据又是100。
  3. 线程1随后被唤醒,并通过比较并替换原则,提交成功。

以上便是ABA问题,线程更新数据并不关心数据中间是否被修改过。

我们可以通过给增加版本号的方式解决这个问题。