什么是乐观锁
??乐观锁在操作共享资源时,它总是抱着乐观的态度进行,它认为自己可以成功地完成操作。当多个线程同时操作一个共享资源时,只有一个线程会成功,失败的线程不会像悲观锁一样在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。 ??乐观锁相比悲观锁来说,不会带来死锁、饥饿等活性故障问题,线程间的相互影响也远远比悲观锁要小。更为重要的是,乐观锁没有因竞争造成的系统开销,所以在性能上也是更胜一筹。
乐观锁的实现原理
??CAS 是实现乐观锁的核心算法,它包含了 3 个参数:V(需要更新的变量)、E(预期值)和 N(最新值)。 ??只有当需要更新的变量等于预期值时,需要更新的变量才会被设置为最新值,如果更新值和预期值不同,则说明已经有其它线程更新了需要更新的变量,此时当前线程不做操作,返回 V 的真实值。
- CAS 如何实现原子操作
在 JDK 中的 concurrent 包中,atomic 路径下的类都是基于 CAS 实现的,如 AtomicInteger 依赖于 Unsafe 类中的操作方法会调用 CPU 底层指令实现原子操作。 - 处理器如何实现原子操作
CAS 是调用处理器底层指令来实现原子操作,处理器和物理内存之间的通信速度要远慢于处理器间的处理速度,所以处理器有自己的内部缓存。处理器提供了总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。目前最新的处理器都支持缓存锁定机制。
优化 CAS 乐观锁
??虽然乐观锁在并发性能上要比悲观锁优越,但是在写大于读的操作场景下,CAS 失败的可能性会增大,如果不放弃此次 CAS 操作,就需要循环做 CAS 重试,这会长时间地占用 CPU。 ??在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。 ??LongAdder 原理是,将热点数据 value 分离成多个单元的 cell,每个 cell 独自维护内部的值,当前对象的实际值由 cell[] 数组中所有的 cell 累计合成。这样,热点就进行了有效的分离,提高了并行度,所以 LongAdder 虽然降低了并发竞争,但是却对实时更新的数据不友好。 ??LongAdder 在操作后的返回值只是一个近似准确的数值,但是 LongAdder 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,LongAdder 并不能取代 AtomicInteger 或 AtomicLong。
性能测试
??基于 JVM 实现的同步锁 Synchronized,AQS 实现的同步锁 Lock 以及 CAS 实现的乐观锁,到底哪一种的性能最好? ??对四种模式下的五个锁 Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock 以及乐观锁 LongAdder 进行压测: ??在读大于写的场景下,读写锁 ReentrantReadWriteLock、StampedLock 以及乐观锁的读写性能是最好的;在写大于读的场景下,乐观锁的性能是最好的,其它 4 种锁的性能则相差不多;在读和写差不多的场景下,两种读写锁以及乐观锁的性能要优于 Synchronized 和 ReentrantLock。 ??CAS 乐观锁在平常使用时比较受限,它只能保证单个变量操作的原子性,当涉及到多个变量时,CAS 就无能为力了,但悲观锁可以通过对整个代码块加锁来做到这点。
|