一文总结Java开发各种锁

对于后端Java开发人员来说,锁主要有Java锁和DB锁。DB锁,请参考一文总结MySQL各种锁。本文试图全面介绍各种Java语言里面的锁。

简介

为什么用锁?保障安全。

注:本文局限于Java语言和MySQL数据库。

Java

具体来说,是用于并发情况下的安全,也是为了解决内存中的一致性,原子性,有序性三种问题。

乐观锁和悲观锁

悲观锁

悲观锁,它觉得每次访问数据都可能被其他线程修改,故而在访问资源时就会对资源进行加锁,用这种方式来保证资源在访问时不会被其他线程修改。因此,其他线程想要获取资源的话就只能阻塞,等到当前线程释放锁后在获取。悲观锁保证资源同时只能一个线程进行操作。
实现有synchronized关键字和Lock的实现类。

乐观锁

乐观锁,是一种思想,主要是两个步骤:冲突检测和数据更新。假定访问数据时不会被修改,不会上锁,但是在提交时会去判断一下是否有别的线程修改当前数据。
原理:CAS。CAS有三个操作数,内存数据V,旧的预期数据A,要修改的数据B。每次进行数据更新时,当且仅当预期值A和内存中的数据V相同时,才将内存中的数据修改为B,否则什么也不做。使用这种机制编写的算法也叫非阻塞算法,标准定义为一个线程的失败或者挂起不影响其他线程的失败或者挂起的算法。方法compareAndSet却是利用JNI来完成CPU指令的操作。
实现方式:版本号和时间戳
API实现:JUC并发包下提供的原子类。
在这里插入图片描述

公平锁和非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,所以能会造成优先级反转或者饥饿现象;synchronized 就是非公平锁,ReentrantLock(使用 CAS 和 AQS 实现) 通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁;非公平锁的吞吐量性能比公平锁大好。

可重入锁

又叫递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
一个线程在执行一个带锁的方法,该方法中又调用另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响
synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁。

面试题:

  1. 可重入锁如果加两把,但只释放一把会出现什么问题?
    程序卡死,线程不能出来,申请几把锁,就需要释放几把锁。
  2. 如果只加一把锁,释放两次会出现什么问题?
    会报错,java.lang.IllegalMonitorStateException。

java的可重入锁用在哪些场合?

分段锁

实质是一种锁的设计策略,不是具体的锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效并发操作;当要 put 元素时并不是对整个 HashMap 加锁,而是先通过 HashCode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在的不是同一个分段,就做到真正的并行插入,统计 size 时就需要获取所有的分段锁才能统计;分段锁的设计是为了细化锁的粒度。

独占锁和共享锁

独占锁,又叫独享锁,指该锁一次只能被一个线程持有;
共享锁,可以被多个线程持有;
synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁;ReentrantLock 的独享锁和共享锁也是通过 AQS 来实现的。

可中断锁

可以响应中断的锁。可中断建立在阻塞等待中断,运行中是无法中断的。synchronized 是不可中断的,Lock 是可中断的。

偏向锁

偏向锁,Biased Locking。大多数情况下,锁不仅不存在多线程竞争,而且总是有同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只需简单的测试一下对象头的 MarkWord 里是否存储着指向当前线程的偏向锁。

但是一旦有第二个线程需要竞争锁,偏向模式立即结束,进入轻量级锁的状态。

优点:偏向锁可以提高有同步但没有竞争的程序性能。

如果锁对象时常被多条线程竞争,那偏向锁就是多余的;在竞争激烈的场合,偏向锁会增加系统负担。偏向锁可以通过虚拟机的参数来控制它是否开启。
JDK1.6后,偏向锁默认开启,会尝试把锁赋给第一个访问它的线程,取消同步块上的synchronized原语。使用-XX:UseBiasedLocking禁用。偏向锁可以提高缓存命中率,但偏向锁也需要记录一些信息,有时候性能会更糟,比如使用某些线程池,同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。

使用偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
BiasedLockingStartupDelay=0设置偏向锁的启动时间为零,即在系统启动时就启用偏向锁。但一般在系统启动时竞争是非常大的,使用它是非常耗时的。
不使用偏向锁:-XX:-UseBiasedLocking,以避免长时间的预热,因为偏向锁并不是直接启动,而是完成初始化阶段后等待5秒钟才开始

偏向锁&轻量级锁

与轻量级锁的区别:轻量级锁是在无竞争的情况下使用 CAS 操作来代替互斥量的使用,从而实现同步;而偏向锁是在无竞争的情况下完全取消同步。
与轻量级锁的相同点:都是乐观锁,都认为同步期间不会有其他线程竞争锁。

互斥锁、自旋锁

互斥锁

锁,默认即是互斥的,又叫阻塞锁。线程的阻塞(WAITING ,挂起)和唤醒(RUNNABLE ,恢复)需要CPU从用户态转为核心态,涉及上下文切换、CPU 抢占等开销。在许多应用中,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。引入自旋锁。

自旋锁

自旋锁是相对于互斥锁的概念。所谓自旋锁,即让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。等待就是执行一段无意义的循环即可(自旋)。自旋锁的线程一直是 RUNNABLE 状态的,一直在那循环检测锁标志位,机制不重复。但是自旋锁加锁全程消耗 CPU,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长。自旋等待的时间,即次数,不能太大。
自旋锁在JDK 1.4.2中引入,默认关闭,可使用-XX:+UseSpinning开启。

自适应自旋锁
在JDK1.6中默认开启。自旋默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。并且加以优化,即自适应自旋锁。自适应,即自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

当一个线程获取锁时,这个锁已经被其他人获取到,那么这个线程不会立马挂起,反而在不放弃CPU使用权的情况下会尝试再次获取锁资源。自旋锁是不公平锁。
优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

区别

最大的区别就是,到底要不要放弃处理器的执行时间。两者都要等待获得共享资源。阻塞锁是放弃CPU时间,进入等待区,等待被唤醒。而自旋锁是一直自旋在那里,时刻的检查共享资源是否可以被访问。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。

如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。互斥锁适用于临界区操作耗时比较长,自旋锁并发量比较高且临界区的操作耗时比较短。

mutex 锁?

轻量级锁&重量级锁

轻量级锁

轻量级锁是相对于重量级锁而言的,而重量级锁就是传统的锁。本质是使用CAS取代互斥同步。

实现原理:

  • 虚拟机为了节约对象的存储空间,对象处于不同的状态下,对象头Mark Word中存储的信息也所有不同,Mark Word 中有个标志位用来表示当前对象所处的状态;
  • 当线程请求锁时,若该锁对象的 Mark Word 中标志位为 01(未锁定状态),则在该线程的栈帧中创建一块名为『锁记录』的空间,然后将锁对象的 Mark Word 拷贝至该空间;最后通过 CAS 操作将锁对象的 Mark Word 指向该锁记录;
  • 若 CAS 操作成功,则轻量级锁的上锁过程成功;
  • 若 CAS 操作失败,再判断当前线程是否已经持有该轻量级锁;若已经持有,则直接进入同步块;若尚未持有,则表示该锁已经被其他线程占用,此时轻量级锁就要膨胀成重量级锁;

前提:轻量级锁比重量级锁性能更高的前提,在轻量级锁被占用的整个同步周期内,不存在其他线程的竞争。
若在该过程中一旦有其他线程竞争,那么就会膨胀成重量级锁,从而除了使用互斥量以外,还额外发生 CAS 操作,因此更慢!

重量级锁

指的是锁的粒度。粒度越小,越轻量级,性能越好。

轻量级锁与重量级锁的比较:

重量级锁是一种悲观锁,每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用互斥同步来保证线程的安全;
轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,所以它不使用互斥同步,而是使用 CAS 操作来获得锁,这样能减少互斥同步所使用的『互斥量』带来的性能开销。

锁消除&锁粗化

锁消除

指JIT编译器在运行时,对一些没有必要同步的代码却同步的锁进行消除。一种彻底的锁优化,可以节省毫无意义的请求锁时间。

锁消除涉及一个技术:逃逸分析,观察某一个变量是否会逃出某一个作用域。

锁粗化

原则上,在编写代码时,总是推荐将同步块(锁粒度)尽可能的小。这样是为了使得需要同步的操作数量小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,这个原则是正确的。

如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁的同步操作也会导致不必要的性能损耗。如果虚拟机探测到很多零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,由多次加锁变成只加锁一次。即加大同步块。JVM可以进行锁粗化优化,可以有效地合并多个相邻的加锁代码块,减少加锁的成本。锁粗化的困难在于,JVM 如何控制粗化的粒度,避免独占锁;

JVM锁粗化和循环

锁升级

锁膨胀,Java SE 1.6 中,锁一共有4个状态,从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。这几个状态会随着竞争情况逐渐升级(即膨胀)。锁升级之后不能降级。

无锁

Lock-Free,最好的锁就是无锁。有锁是悲观的操作,假设竞争是存在的,所以要加锁。无锁是乐观操作,假设没有竞争存在。无锁的一种实现方式CAS,Compare And Swap,非阻塞的同步,不会去等待,上来就会不断尝试,尝试失败在尝试。要么失败要么直接成功,成功则退出。

参考

posted @   johnny233  阅读(29)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
历史上的今天:
2019-02-26 Java学习之NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError
点击右上角即可分享
微信分享提示