史上最全 Java 中各种锁的介绍

锁的分类介绍
乐观锁与悲观锁锁的一种宏观分类是乐观锁与悲观锁 。乐观锁与悲观锁并不是特定的指哪个锁(JAVA 中也没有那个具体锁的实现名就叫
乐观锁或悲观锁),而是在并发情况下两种不同的策略 。
乐观锁(Optimistic Lock)就是很乐观,每次去拿数据的时候都认为别人不会修改 。所以不会上锁 。但是如果想要更新数据,
则会在更新之前检查在读取至更新这段时间别人有没有修改过这个数据 。如果修改过,则重新读取,再次尝试更新,循环上述
步骤直到更新成功(当然也允许更新失败的线程放弃更新操作) 。
悲观锁(Pessimistic Lock)就是很悲观,每次去拿数据的时候都认为别人会修改 。所以每次都在拿数据的时候上锁 。
这样别人拿数据的时候就会被挡住,直到悲观锁释放,想获取数据的线程再去获取锁,然后再获取数据 。
史上最全 Java 中各种锁的介绍

文章插图
 
悲观锁阻塞事务,乐观锁回滚重试,它们个有优缺点,没有好坏之分,只有适应场景的不同区别 。比如:乐观锁适合用于写
比较少的情况下,即冲突真的很少发生的场景,这样可以省去锁的开销,加大了系统的整个吞吐量 。但是如果经常产生冲突,上层
应用会不断的进行重试,这样反而降低了性能,所以这种场景悲观锁比较合适 。
总结:乐观锁适合写比较少,冲突很少发生的场景;而写多,冲突多的场景适合使用悲观锁 。
乐观锁的基础 --- CAS
在乐观锁的实现中,我们必须要了解的一个概念:CAS 。
什么是 CAS 呢? Compare-and-Swap,即比较并替换,或者比较并设置 。
  • 比较:读取到一个值 A,在将其更新为 B 之前,检查原值是否为 A(未被其它线程修改过,这里忽略 ABA 问题) 。
  • 替换:如果是,更新 A 为 B,结束 。如果不是,则不会更新 。
上面两个步骤都是原子操作,可以理解为瞬间完成,在 CPU 看来就是一步操作 。
有了 CAS,就可以实现一个乐观锁:
public class OptimisticLockSample{public void test(){ int data = https://www.isolves.com/it/cxkf/yy/JAVA/2019-12-05/123; // 共享数据// 更新数据的线程会进行如下操作 for (;;) { int oldData = data; int newData = doSomething(oldData);// 下面是模拟 CAS 更新操作,尝试更新 data 的值 if (data == oldData) { // compare data = newData; // swap break; // finish } else { // 什么都不做,循环重试 } }}/** ** 很明显,test() 里面的代码根本不是原子性的,只是展示了下 CAS 的流程 。* 因为真正的 CAS 利用了 CPU 指令 。** */}在 Java 中也是通过 native 方法实现的 CAS 。
public final class Unsafe {...public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);...} 上面写了一个简单直观的乐观锁(确切的来说应该是乐观锁流程)的实现,它允许多个线程同时读取(因为根本没有加锁操作),如果更新数据的话,
有且仅有一个线程可以成功更新数据,并导致其它线程需要回滚重试 。CAS 利用 CPU 指令,从硬件层面保证了原子性,以达到类似于锁的效果 。
从乐观锁的整个流程中可以看出,并没有加锁和解锁的操作,因此乐观锁策略也被称作为无锁编程 。换句话说,乐观锁其实不是"锁",
它仅仅是一个循环重试的 CAS 算法而已 。
自旋锁synchronized 与 Lock interface
Java 中两种实现加锁的方式:一种是使用 synchronized 关键字,另一种是使用 Lock 接口的实现类 。
在一篇文章中看到一个好的对比,非常形象,synchronized 关键字就像是自动挡,可以满足一切的驾驶需求 。
但是如果你想要做更高级的操作,比如玩漂移或者各种高级的骚操作,那么就需要手动挡,也就是 Lock 接口的实现类 。
而 synchronized 在经过 Java 每个版本的各种优化后,效率也变得很高了 。只是使用起来没有 Lock 接口的实现类那么方便 。
synchronized 锁升级过程就是其优化的核心:偏向锁 -> 轻量级锁 -> 重量级锁
class Test{ private static final Object object = new Object();public void test(){ synchronized(object) { // do something}} }使用 synchronized 关键字锁住某个代码块的时候,一开始锁对象(就是上述代码中的 object)并不是重量级锁,而是偏向锁 。


推荐阅读