浅析CAS

CAS基本使用

以ReentrantLock为例,观察CAS基本使用。

class ReentrantLockExample {
    int a = 0;
    // 非公平锁
    ReentrantLock lock = new ReentrantLock(false);

    public void writer() {
        // 获取锁
        lock.lock();
        try {
            a++;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public void reader() {
        // 获取锁
        lock.lock();
        try {
            int i = a;
            // ...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

ReentrantLock类图

浅析CAS_第1张图片

使用公平锁时,加锁方法lock()调用轨迹如下:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires)

在第4步真正开始加锁,源码如下:

        protected final boolean tryAcquire(int acquires) { // acquires 调用时值为1
            final Thread current = Thread.currentThread();
            // 加锁前,先获取锁,读volatile变量(state初始值为0
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) { // CAS比较并交换
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

lock()加锁前首先读volatile变量state。

在使用公平锁时,解锁方法unlock()调用轨迹如下:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)
  3. Sync:tryRelease(int releases)

在第3步真正开始释放锁,源码如下:

        protected final boolean tryRelease(int releases) { // 调用时值为1
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 此时加完锁,state为1,1 - 1 = 0 nextc = 0
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            // 释放锁的最后,写volatile变量state,恢复为0
            setState(nextc);
            return free;
        }

unlock()在释放锁的最后写volatile变量state。

非公平锁的释放和公平锁完全一样,仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下:

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect, int update)

浅析CAS_第2张图片

在第3步真正开始加锁,源码如下:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

该方法以原子操作的方式更新state变量(CAS)。

原理解析

JDK文档对上述方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。

编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序(插入内存屏障)。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

是一个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp。

这个本地方法的最终实现在openjdk的如下位置:openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于Windows操作系统,X86处理器)。intel X86处理器的源代码片段:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, 
        jint     compare_value) {
     // alternative for InterlockedCompareExchange
     int mp = os::is_MP();
     __asm {
       mov edx, dest
       mov ecx, exchange_value
       mov eax, compare_value
       LOCK_IF_MP(mp)
       cmpxchg dword ptr [edx], ecx
    }
}

程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明

  1. 确保对内存的读-改-写操作原子执行。Intel使用缓存锁定(Cache Locking)来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。(某些处理器可能会锁总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。)
  2. 禁止该指令,与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。

第2点和第3点所具有的内存屏障效果,足以让CAS同时实现volatile读和volatile写的内存语义。

公平锁和非公平锁的内存语义总结:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

CAS三大问题

JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

线程安全(原子类)/ 不安全 i++操作:

public class Counter {
    private final AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            // 创建100个线程
            Thread t = new Thread(() -> {
                // 每个线程 数字 + 10000
                for (int i = 0; i < 10000; i++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
        // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 期望 1000000
        System.out.println(cas.i);
        System.out.println(cas.atomicI.get());
        System.out.println("consume time " +(System.currentTimeMillis() - start) + "ms");
    }

    /**
     * 使用CAS实现线程安全计数器
     */
    private void safeCount() {
//        for (; ; ) {
//            int i = atomicI.get();
//            boolean suc = atomicI.compareAndSet(i, ++i);
//            if (suc) {
//                break;
//            }
//        }

        // 两种都可以
        atomicI.incrementAndGet();
    }

    /**
     * 非线程安全计数器
     */
    private void count() {
        i++;
    }
}

测试结果:

浅析CAS_第3张图片

原子类可以保证线程安全操作。原子类虽好但也存在问题。

ABA问题

一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题

该类compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(
               V   expectedReference,        // 预期引用
               V   newReference,             // 更新后的引用
               int expectedStamp,            // 预期标志
               int newStamp                  // 更新后的标志
)

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。

pause指令有两个作用

第一延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;

第二避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率

相关CPU术语:

浅析CAS_第4张图片

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

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

你可能感兴趣的:(#,juc,java)