Java之CAS原理及实现

1 CAS概念

CAS(Compare and swap)比较和交换,属于硬件同步语,从硬件层面保证了原子性。

CAS(V,A,B)包含三个参数,分别为内存位置(V)、旧值(A)、新值(B),在操作期间先对旧值进行比较,若没有发生变化,则交换为新值,若发生了则不交换。

1.1 原子操作

原子性,即原子性操作,原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不能被打乱,也不可以被切割,在整个步骤中资源要保持一致。

具体实例:

public class Counter {
    volatile int i = 0;
    public void add() {
        i++;
    }
}
public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        final Counter ct = new Counter();

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.i);
    }
}

循环开启十个线程,每个线程循环i++操作,按道理来说,循环结束时i的值应该为100000,可是实际操作中,绝大部分值小于100000。为什么呢,这里就要提到原子操作了,虽然i++循环了十万次,但是i++并不是一个原子性操作。

于idea终端(Terminal)反编译查看i++的二进制字节码:
1:cd进入Counter类target下的目录,我这里是
cd D:\workspace_2019\subject-1\target\classes\com\study\cas
2:javap -v -p Counter.class命令进行反编译

这里我们看到反编译后的add方法:

public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/study/cas/Counter;

重点是这四句:

 2: getfield      #2                
 5: iconst_1
 6: iadd
 7: putfield      #2                  
  1. getfield:获取对象字段的值,即获取i的初始值并入栈。
  2. iconst_1:1(int)值入栈,即将数值1压入栈。
  3. iadd: 将栈顶两int类型数相加,结果入栈。
  4. putfield:给对象字段赋值,即将相加后的值重新赋给对象i。

那么又为什么会产生原子性问题呢?

可以看到当多个线程同时运行时,线程1还未来得及将累加后的值重新赋给变量i,线程2又拿到了i的初始值,这就会导致两个线程执行完后i的值本应该是2,实际却为1,违反了不可被切割原则,资源被改变,这就是问题产生的原因。

2 CAS实现

Java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法来实现CAS。
修改Counter类为CounterUnsafe类:

public class CounterUnsafe {
    volatile int i = 0;

    private static Unsafe unsafe = null;

    //i字段的偏移量
    private static long valueOffset;

    static {
        //unsafe = Unsafe.getUnsafe();
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            Field fieldi = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(fieldi);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add() {
        for (;;){
            int current = unsafe.getIntVolatile(this, valueOffset);
            if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1))
                break;
        }
    }
}

Unsafe不能直接使用,只能通过反射获取,调用compareAndSwapInt方法,传入偏移量,旧值,新值,实现原子性操作。
此时,重新测试:

public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        final CounterUnsafe ct = new CounterUnsafe();

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.i);
    }
}

多次执行结果都是100000正确值。

JDK中J.U.C包提供了许多Atomic原子操作类,封装了CAS方法以供调用,如AtomicInteger,AtomicIntegerArray,AtomicStampedReference等等。

通过AtomicInteger来解决CAS问题:

public class CounterAtomic {
    //volatile int i = 0;
    AtomicInteger i = new AtomicInteger(0);

    public void add() {
        i.incrementAndGet();
    }
}
public class CounterTest {
    public static void main(String[] args) throws InterruptedException {
        final CounterAtomic ct = new CounterAtomic();

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.i);
    }
}

多次运行结果都为100000。
查看源码可发现AtomicInteger类中就是使用了unsafe的CAS操作。

  /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

3 CAS问题

尽管CAS解决了原子性问题,但也不可避免存在了一些问题。

  1. 自旋的实现会增加CPU的资源消耗
  2. 仅能针对单个变量,不能用于多个变量来实现原子操作
  3. ABA问题

3.1 ABA问题原理

在这里插入图片描述
当线程1执行完第三步,线程2再执行时,原本应该失败的CAS操作却成功了,这就是ABA问题,因为变量i的值从A变成B之后又变回了A,而CAS操作只识别旧值A没变,为了解决这一问题,就要引入版本号的概念。

3.2 ABA解析

AtomicStampedReference类提供了版本号参数避免ABA问题。

在这里插入图片描述
如图,栈中原有A,B元素,线程1线程2都想要TopA,线程2先运行,当线程2运行结束时,栈顶依旧是A,线程1执行Top CAS(A,B)依旧可以成功,但这是错误的,A.next此时已经变成了C。

错误示例代码如下:

// 实现一个 栈(后进先出)
public class Stack {
    // top cas无锁修改
    AtomicReference<Node> top = new AtomicReference<Node>();

    public void push(Node node) { // 入栈
        Node oldTop;
        do {
            oldTop = top.get();
            node.next = oldTop;
        }
        while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
    }


    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {

        Node newTop;
        Node oldTop;
        do {
            oldTop = top.get();
            if (oldTop == null) {   //如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) {    //模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        }
        while (!top.compareAndSet(oldTop, newTop));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {

        Stack stack = new Stack();
        // ConcurrentStack stack = new ConcurrentStack();

        stack.push(new Node("B"));      //B入栈
        stack.push(new Node("A"));      //A入栈

        Thread thread1 = new Thread(() -> {
            Node node = stack.pop(800);
            System.out.println(Thread.currentThread().getName() +" "+ node.toString());

            System.out.println("done...");
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            LockSupport.parkNanos(1000 * 1000 * 300L);

            Node nodeA = stack.pop(0);      //取出A
            System.out.println(Thread.currentThread().getName()  +" "+  nodeA.toString());

            Node nodeB = stack.pop(0);      //取出B,之后B处于游离状态
            System.out.println(Thread.currentThread().getName()  +" "+  nodeB.toString());

            stack.push(new Node("C"));      //C入栈
            stack.push(nodeA);                    //A入栈

            System.out.println("done...");
        });
        thread2.start();

        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);


        System.out.println("开始遍历Stack:");
        Node node = null;
        while ((node = stack.pop(0))!=null){
            System.out.println(node.value);
        }
    }
}

运行程序后发现,A.next指向了游离的B。为了解决ABA问题,AtomicStampedReference提供了版本号的引入,就算线程2再次将A放入栈顶,但由于版本号变化,CAS操作将不能再执行成功。

将Stack类替换为使用了版本号的ConcurrentStack类再次测试:

public class ConcurrentStack {
    // top cas无锁修改
    //AtomicReference<Node> top = new AtomicReference<Node>();
    AtomicStampedReference<Node> top =
            new AtomicStampedReference<>(null, 0);

    public void push(Node node) { // 入栈
        Node oldTop;
        int v;
        do {
            v = top.getStamp();
            oldTop = top.getReference();
            node.next = oldTop;
        }
        while (!top.compareAndSet(oldTop, node, v, v+1)); // CAS 替换栈顶
    }


    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {

        Node newTop;
        Node oldTop;
        int v;

        do {
            v = top.getStamp();
            oldTop = top.getReference();
            if (oldTop == null) {   //如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) {    //模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        }
        while (!top.compareAndSet(oldTop, newTop, v, v+1));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {

        // Stack stack = new Stack();
        ConcurrentStack stack = new ConcurrentStack();

        stack.push(new Node("B"));      //B入栈
        stack.push(new Node("A"));      //A入栈

        Thread thread1 = new Thread(() -> {
            Node node = stack.pop(800);
            System.out.println(Thread.currentThread().getName() +" "+ node.toString());

            System.out.println("done...");
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            LockSupport.parkNanos(1000 * 1000 * 300L);

            Node nodeA = stack.pop(0);      //取出A
            System.out.println(Thread.currentThread().getName()  +" "+  nodeA.toString());

            Node nodeB = stack.pop(0);      //取出B,之后B处于游离状态
            System.out.println(Thread.currentThread().getName()  +" "+  nodeB.toString());

            stack.push(new Node("C"));      //C入栈
            stack.push(nodeA);                    //A入栈

            System.out.println("done...");
        });
        thread2.start();

        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);


        System.out.println("开始遍历Stack:");
        Node node = null;
        while ((node = stack.pop(0))!=null){
            System.out.println(node.value);
        }
    }
}

此时运行结束,A.next指向正确的C,从而解决ABA问题。

4 线程安全的相关概念

CAS的存在避免了原子性问题,也就是保证了线程的安全,另一安全性问题----可见性,这里不做描述,主要说几个名词概念。

  1. 竞态条件:如果程序运行顺序的改变会影响最终结果,就说存在竞态条件。大多数竞态条件的本质,就是基于某种可能失效的观察结果来做出判断或执行某个计算。
  2. 临界区:存在竞态条件的代码区域就叫临界区。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值