什么是乐观锁?

​ 乐观锁,顾名思义,就是说在操作共享资源时,它总是抱着乐观的态度举行,它以为自己可以乐成地完成操作。但实际上,当多个线程同时操作一个共享资源时,只有一个线程会乐成,那么失败的线程呢?乐观锁不会像消极锁一样在操作系统中挂起,而仅仅是返回,而且系统允许失败的线程重试,也允许自动放弃退出操作。

​ 以是,乐观锁相比消极锁来说,不会带来死锁、饥饿等活性故障问题,线程间的相互影响也远远比消极锁要小。更为重要的是,乐观锁没有因竞争造成的系统开销,以是在性能上也是更胜一筹。

乐观锁的实现原理

​ CAS 是实现乐观锁的焦点算法,它包含了 3 个参数:V(需要更新的变量)、E(预期值)和 N(最新值)。

​ 只有当需要更新的变量即是预期值时,需要更新的变量才会被设置为最新值,若是更新值和预期值差别,则说明已经有其它线程更新了需要更新的变量,此时当前线程不做操作,返回 V 的真实值。

CAS 若何实现原子操作

​ 在 JDK 中的 concurrent 包中,atoMic 路径下的类都是基于 CAS 实现的。AtomicInteger 就是基于 CAS 实现的一个线程平安的整型类。下面我们通过源码来了解下若何使用 CAS 实现原子操作。

​ 我们可以看到 AtomicInteger 的自方式 getAndIncrement 是用了 Unsafe 的 getAndAddInt 方式,显然 AtomicInteger 依赖于内陆方式 Unsafe 类,Unsafe 类中的操作方式会挪用 CPU 底层指令实现原子操作。

//基于CAS操作更新值
puBLic final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//基于CAS操作增1
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
//基于CAS操作减1
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

优化 CAS 乐观锁

​ 虽然乐观锁在并发性能上要比消极锁优越,但是在写大于读的操作场景下,CAS 失败的可能性会增大,若是不放弃此次 CAS 操作,就需要循环做 CAS 重试,这无疑会长时间地占用 CPU。

​ 在 Java7 中,通过以下代码我们可以看到:AtomicInteger 的 getAndSet 方式中使用了 for 循环不断重试 CAS 操作,若是长时间不乐成,就会给 CPU 带来非常大的执行开销。到了 Java8,for 循环虽然被去掉了,但我们反编译 Unsafe 类时就可以发现该循环其实是被封装在了 Unsafe 类中,CPU 的执行开销依然存在。

   public final int getAndSet(int newValue) {
        while (true) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

​ 在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,价值就是会消耗更多的内存空间。

测试对比

​ 我们分别在“读多写少”、“读少写多”、“读写差不多”这三种场景下举行测试。

​ 对三种模式下的五个锁 Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock 以及乐观锁 LongAdder 举行压测。压测效果如下图:

​ 通过以上效果,我们可以发现:

在写大于读的场景下,乐观锁的性能是最好的,其它 4 种锁的性能则相差不多;

在读和写差不多的场景下,两种读写锁以及乐观锁的性能要优于 Synchronized 和 ReentrantLock。在读大于写的场景下,读写锁 ReentrantReadWriteLock、StampedLock 以及乐观锁的读写性能是最好的。