锁分类
按照不同维度,或者说不同的视角.锁可以分为许多类别
乐观锁 & 悲观锁
乐观锁
乐观锁,是对并发锁的乐观估计.即认为实际过程中并发写几率无或很低
乐观锁本质上是无锁.它是提前取出类似版本号之类的,然后在提交时去比较版本号看是否发生变更,如果不匹配则不更新,并交给提交方选择诸如重试或抛出异常等不同策略
最常见的乐观锁比如CAS,提交变更时同时上交持有的原始值.要求如果原始值相同即更新
悲观锁
悲观锁,是对并发锁的悲观估计.即认为实际过程中并发写非常高
悲观锁就是我们通常意义上的并发锁.它会在写之前提前独占资源,直到完成后再重新释放资源
重量锁 & 轻量锁 &偏向锁
对象同步块头
标志位 | 状态 |
---|---|
01 | 未锁.可偏向 |
00 | 轻量锁 |
10 | 重量锁 |
重量锁
重量锁,或者说内核锁.是一种操作系统级别的锁
也就是它的锁是发生在操作系统层面,这时的锁是真正的锁,会触发操作系统级别的线程的阻塞和切换
轻量锁
轻量锁,是一种应用程序级别的锁
轻量锁的本质是一种自旋锁的实现
轻量锁的实现依赖对象的同步块头.它是在自造循环监视锁对象的同步块头,尝试抢占写入.一旦抢占成功就标记为00,即视为抢占成功.如果发现同步块头已被写入,就检查同步快头的中是否指向自己.如果指向自己则依然视为抢占成功(可重入),但如果有第二个线程尝试抢占,则轻量锁会膨胀为重量锁
轻量锁的设计依赖”绝大部分的情况下不存在竞争”,所以用CAS避开了重量锁.但如果存在大量竞争,轻量锁除了CAS自旋以外依然会进入重量锁,所以在存在大量竞争的情况下,轻量锁反而不如重量锁
偏向锁
轻量锁是尝试使用CAS去尝试消除互斥,偏向锁则连CAS都放弃
偏向锁描述的是一种极端情况,即不存在竞争的情况下,让第一个获得锁的线程可以直接进入.偏向锁的偏是偏袒的意思,偏袒第一个获得锁的线程
偏向锁整个类似无锁,进入退出都无需占有或释放更新对象头.(偏向锁会在竞争时释放,但因为竞争此时偏向锁早已升级为轻量锁了)
但轻量锁的进入退出都必须通过CAS更新对象头
偏向锁在1.6之后是默认开启,如果应用程序本身竞争是非常频繁的,偏向锁本身可能反而会降低性能,此时可以 -XX:-UseBiasedLocking
去除偏向锁,当然,这需要事实上的测试才能为准
偏向锁->轻量锁->重量锁
偏向锁,轻量锁,重量锁是同一个锁的不断升级,是不冲突的概念
当对象的同步块从无人抢夺到第一个线程占有,此时的锁就是偏向锁.第一个线程会抢夺标记同步块,同步块也会指向该线程,也就是偏向第一个线程,此时第一个线程可以无锁进入
当有第二个线程尝试去抢夺标记时,当前偏向即宣告结束.此时会根据当前同步是锁定还会未锁定(也就是第一个线程是否已经退出)进行判断,如果当前未锁定(即第一个线程已经完全释放了),此时会再次变为偏向锁,偏向这第二个线程(事实上算第一个,因为上一个已经完全释放了),如果此时是继续锁定状态,此时就会升级为轻量锁,进行CAS自旋
如果轻量锁在经过多次自旋后依然失败,轻量锁就会再次升级.此时就是实实在在的重量锁了.此时的竞争失败的锁会进入阻塞状态等待唤醒
锁消除
锁消除是一种执行优化概念
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
在这段代码中StringBuffer作为线程安全拼接,其内是有同步块参与的.但事实上这段代码会以无锁方式执行.因为整个StringBuffer的作用块都被局限在concatString栈帧中,其外没有任何方法可以影响到这个StringBuffer,所以这个锁会被消除成无锁使用
注意锁消除是1.6之后的功能
锁膨胀
锁膨胀也是一个相对锁消除的执行优化概念
一般来说,程序在使用同步块时都会力求把作用域放得尽可能小,但如果锁在循环体中之类的对同一个对象反复加锁,就会把锁膨胀来避免,因为即使没有线程竞争,频繁的进行互斥加锁也是一种不必要的损耗
注意锁消除是1.6之后的功能
可重入锁
如果同一个线程,可以在拿到锁的情况可以在不同的时间内都运行进入,则为可重入锁
比如 synchronized ,拿到锁后续在不同的栈帧中都可以重复进入,就是可重入锁
比如 CAS就是不可重入锁,CAS的每一次进入都是一次独立自旋
公平锁 & 非公平锁
公平锁是尽量以请求锁的顺序来分配锁(第一个请求锁的优先拿到锁)
比如ReentrantReadWriteLock,构造中可以指定是公平锁还是非公平锁
非公平锁就是始终以竞争姿态抢夺,不保证锁的分配顺序.
比如synchronized,就是非公平锁
一般来说,非公平锁的性能要优于公平锁,但非公平锁在极端情况可能有始终拿不到锁的情况
死锁 & 活锁
活锁
活锁是指没有被阻塞,但由于某些条件没有被满足一直重试
活锁的危害
活锁是一种务必要注意的地方.活锁空转CPU,即浪费CPU能力又实际不产生任何价值,但又不会抛出任何异常很难排查
但某些情况活锁是可能出现的.本质上,自旋锁就是一种活锁.活锁一定要注意的是必须保证在某个条件满足时能够退出,并且保证这种退出条件是可以被出现的.
死锁
出现两个或多个线程,因为争夺资源而互相等待对方释放
*死锁的四个必要条件 *
- 互斥
资源是独占且排它.线程本身占有的资源无法同时分享给其它线程 - 占有并等待
线程已占有的资源并未完全,必须等待另外部分资源 - 无法被剥夺
线程已占用的资源也无法被其它线程剥夺,必须等待线程自身释放 - 循环等待
两个线程所占有和欠缺的资源,刚好处在对方线程
破坏死锁条件而避免死锁
- 破坏互斥
资源是非互斥状态则不会产生死锁.比如各自持有独立副本 - 破坏占有并等待
因为只持有部分资源而必须等待.可以一次性分配全部所需资源而避免等待 - 破坏无法被剥夺
存在一个第三方的超级线程,在满足一定条件的情况下强行释放死锁线程已占用的资源 - 破坏循环等待
合理的调度顺序,破坏循环等待对方的情况