一、JDK8存在的锁
二、锁的特性
三、开发难度+总结
四、synchronized详析
一、JDK8存在的锁
synchronized
StampedLock
ReentrantLock
ReentrantReadWriteLock
PS: 下面内容测试的结果不是十分正确。第一,测试的jdk是1.6,而不是1.8.测试的没有关闭UseBiasedLocking(偏向锁)
二、锁的特性
1 锁升级(十二分)
多种强度的锁方式。优先使用性能消耗低的方式,当当前方式不可用或者不使用,改变成消耗比当前大的锁方式。 是一种十分灵活,智能的性能优化方案,对非特定场景十分适合。不需要开发者关注锁优化,只需要关注业务。
2 重入(十二分)
同一个锁, 重入是应付锁复杂使用情况下的一把利器,是锁性能重点。a与b方式使用同一个锁。a调用b,需要两个锁指令,而重入的解决方案是a执行锁指令,当a调用b方法的时候,不使用锁指令。那么锁的开销减少一半。重入得越多锁开销减少越多。 有测试哪里,需要大家自己测试。 下面的链接中性能测试其实已经给了答案,不知道哪位大神,可以把答案告诉打杂的老胡
3 读写(六分)
分读写锁,读锁与读锁之间可以共享。在读多写少的场景,提高锁的性能 下面博客有对读写的性能测试:http://www.inter12.org/archives/292
4 公平
为了性能,不按上锁的循序获得锁,即不公平锁。按照上锁的循序获得锁,即公平锁。 公平不是为了优先级 下面博客有对公平的性能测试:https://yq.aliyun.com/articles/48612
5 自动释放(十二分)
不用手动调用unLock系列释放锁的方法。解决在复杂的开发体系(业务复杂,开发人员能力参差不齐,细节无视与混淆,测试困难)中,锁操作问题。 异常释放,谁来释放。
6. 锁等待(六分)
当其他线程获得锁之后,等待固定时间之后,还没有获得锁就不在争夺锁。
线程中断(一分)
三、开发难度
特性支持
锁应用环境
业务模块基本不需要关注锁,需要锁的地方都应该使用synchronized,高级以下程序员都使用的数据库锁,或者其他库锁。打杂的目前在业务系没有用过java锁, 功能模块的开发与维护基本是高级或者研发人员,用到锁的地方不多,绝对大部分是使用的synchronized与ReentrantLock,在spring的代码里面只见到许多synchronized,ReentrantLock还没见过 软件系比如netty,dubbo,大量使用ReentrantLock,一些独立的服务比如rocketmq,核心业务重写了ReentrantLock,还有设计自己的加锁机制
读写类型
业务模块基本不需要关注锁,需要锁的地方都应该使用synchronized,高级以下程序员都使用的数据库锁,或者其他库锁。打杂的目前在业务系没有用过java锁, 功能模块的开发与维护基本是高级或者研发人员,用到锁的地方不多,绝对大部分是使用的synchronized与ReentrantLock,在spring的代码里面只见到许多synchronized,ReentrantLock还没见过 软件系比如netty,dubbo,大量使用ReentrantLock,一些独立的服务比如rocketmq,核心业务重写了ReentrantLock,还有设计自己的加锁机制
全写操作目前发现最多的就是计数器,计数器建议使用jdk8的LongAdders(计数器),性能超级好。注意任何计数器无法保证绝对的精确性。
ReentrantLock与ReentrantReadWriteLock的写性能一样。
总结
如果要对特性重要进行排序,要排除对性能极限要求的情况,可以得到以下结论:
重入>锁升级>自动释放>锁等待超时>公平>读写>线程中断
在繁多,复杂的方法,代码,逻辑之间相互调用。谁也不知道,哪个方法,哪段代码使用了锁,一不小心死锁。所以重入是最重要的一点。 除非资深研发人员否则其他人员不应该使用StampedLock
锁升级可以做基本性能方面优化,就交给锁了,可以让锁性能在个个场景都可以保持较好的状态,从而减少锁开发与维护的工作量
自动释放对初级,中级或者高级开发来说,是一个避免出现锁问题的利器,保障开发简单,顺利。不用担心哪里忘记释放锁,从而造成锁问题
锁等待超时是防止无限锁等待而造成线程资源无限占用与线程池无线程可用的情况,从而让应用无法提供服务。是高可用服务保障的利器
复杂的环境下,不知道哪个方法,哪个代码使用了读锁还是写锁。太多未知与细节,十分头疼,需要大量的时间与精力处理读写关系,得不偿失。
做了这么多年开发与研发。感觉性能较好的情况下,不出问题与开发维护方便应该放在对性能高度最求的前面。尤其是线上问题,应该避免出现。
从上面的对比分析,synchronized的得分与评价是最高的,ReentrantLock其次, 不建议使用ReentrantReadWriteLock,禁止使用StampedLock。
四、synchronized详析
synchronized 是java关键字,jvm内部实现。所以jvm可以对synchronized进行优化
每个jdk版本synchronized性能不一样,版本越高的性能越好。jdk1.6与jdk1.7之间的性能差距十分大。
synchronized操作简单,jvm自动优化性能
synchronized详析锁的方式
运行代码:
运行结果:
从上面的执行可以是否发现一个问题,答应是乱序的,自增数据是乱序的。
很多人认为:绝对是java设计的失误...使用一个图片来逻辑推理下:
java与jvm绝对没有错
synchronized是上锁,这点绝对没有问题
那synchronized锁了什么了?
这是我们讨论的论题,也是一个容易犯错的问题。
演示代码,有四个方法。
代码解读
有四个方法分别是静态方法,非静态方法,两个方法里面有synchronized block。四个方法分别组合,测试方法的互斥行。输出内容是按照调用方法的循序执行的,synchronized block方法的输出结果在synchronized 方法之后,那么表示两个方法是互斥的。组合:https://yq.aliyun.com/articles/430975?spm=5176.11065265.1996646101.searchclickresult.2926661eTTKvTF
结论:
synchronized关键字标记在静态方法上是锁当前的class对象。
public synchronized static void XXXXX(){// 锁的对象是 当前的 class}
synchronized关键字标记在非静态方法上是锁当前的实例对象(this)
public synchronized void XXXXX(){// 锁的对象是 当前的 this}
总结
jdk版本越高synchronized的性能越好
这是阿里大神fashjson与driud的作者温 对synchronized简单总结
背景:
某日,对某个优化过的系统进行,上线前的性能压测,压测结果大大的出乎人意料,优化之后比优化之前的TPS只多200+。在16cpu的服务器不应该出现这样的情况。
问题排查:
是不是接口中数据库操作的问题,MySQL通用日志里记录的sql基本一致,慢日志里面没有记录接口操作的sql。
是不是测试人员的测试数据十分重复,更新操作造成锁超时,准备排除锁超时情况,测试人员与业务开发人员反馈,查询接口也一样,数据状态良好
是不是代码问题,分析最后的此时结果,发现所有压测接口都这样。包括简单条主键查询的SQL Why? 奇迹了,数据库与应用一切正常啊。被逼无赖,在每个核心地方输出调用时间,也没问题。发现所有的接口的使用了RateLimiter的acquire方法,深入一看,有synchronized。每次接口的调用都会进入下面的代码,而每次都会有锁争夺。
解决方案:
高并发下synchronize造成jvm性能消耗详析
jvm对synchronized代码块的优化
google guava 的RateLimiter 限流的核心计算代码使用的synchronized,google大神都证明了synchronized的优秀
公平锁与不公平锁:
在一般竞争情况下,两者的性能可以理解为相等
在极高竞争下,不公平锁的性能是可能是公平锁的十几倍
ReentrantReadWriteLock 死锁现象
背景: 某个深夜,老胡在看ReentrantReadWriteLock源码,想用ReentrantReadWriteLock代替ReentrantLock提高性能,反复的看调用流程与实现细节(看了两个多小时)
脑海慢慢呈现整个调用流程与实现细节的流程与逻辑图,发现不对劲啊,可能一不小心出现死锁。
在发现死锁现象同一个深夜,老胡在仔细反复的看公布与不公平,读写锁的细节。反复的看调用流程与实现细节,一边准备与周公喝茶了,一个低头砸到桌子上,脑海里整个调用流程与实现细节的流程与逻辑图砸出一个闪光,找到一个问题
在高并发下,很多线程争夺一个锁的时候,在队列的里面的锁可能能难争夺到锁,争夺不到,会饿死啊。
锁使用总结:在高并发情况下,使用tryLock(超时)杜绝 饥饿。没获得锁,可以直接异常与返回异常结果
能不使用锁,绝对不要使用...........
注意上面说的细节,比如synchronized锁的对象等
优先使用synchronized关键字,能不用使用synchronized块就不使用
高并发的情况下使用synchronized,麻烦关闭偏向锁-XX:-UseBiasedLocking
减少锁粒度
优先使用重入锁,禁止非重入锁StampedLock的使用
一定要分析场景,在选择对应的锁,如果不分析只能使用synchronized
tryLock(超时) 是处理死锁与饥饿的神器。
一个class或者一个实例里面只允许一个锁。两个锁容易出现死锁。这个锁必须能重入