Java锁细节整理

2019-04-24 18:38:23 浏览数 (1)

导读 作者:老胡(微信号:laohu20160411) 很熟悉的名字吧,没错,就是2018年在知数堂开了两场公开课的老胡老师

点击回顾公开课精彩视频与讲义公开课发布:《Alibaba RocketMQ详析》by老胡 公开课发布:《分布式消息中间件核心要点》by老胡 本文简介:锁是在开发的过程无法避免的问题。也是面试常问的问题。 本文比较详细的解决了java中的锁,记住是锁。

一、JDK8存在的锁

  1. synchronized
  2. StampedLock
  3. ReentrantLock
  4. ReentrantReadWriteLock

PS: 下面内容测试的结果不是十分正确。第一,测试的jdk是1.6,而不是1.8.测试的没有关闭UseBiasedLocking(偏向锁)

二、锁的特性

  1. 锁升级(十二分)

多种强度的锁方式。优先使用性能消耗低的方式,当当前方式不可用或者不使用,改变成消耗比当前大的锁方式。 是一种十分灵活,智能的性能优化方案,对非特定场景十分适合。不需要开发者关注锁优化,只需要关注业务。

2. 重入(十二分)

同一个锁, 重入是应付锁复杂使用情况下的一把利器,是锁性能重点。a与b方式使用同一个锁。a调用b,需要两个锁指令,而重入的解决方案是a执行锁指令,当a调用b方法的时候,不使用锁指令。那么锁的开销减少一半。重入得越多锁开销减少越多。 有测试哪里,需要大家自己测试。 下面的链接中性能测试其实已经给了答案,不知道哪位大神,可以把答案告诉打杂的老胡

  1. 读写(六分)

分读写锁,读锁与读锁之间可以共享。在读多写少的场景,提高锁的性能 下面博客有对读写的性能测试:http://www.inter12.org/archives/292

4.公平

为了性能,不按上锁的循序获得锁,即不公平锁。按照上锁的循序获得锁,即公平锁。 公平不是为了优先级 下面博客有对公平的性能测试:https://yq.aliyun.com/articles/48612

5. 自动释放(十二分)

不用手动调用unLock系列释放锁的方法。解决在复杂的开发体系(业务复杂,开发人员能力参差不齐,细节无视与混淆,测试困难)中,锁操作问题。 异常释放,谁来释放。

6. 锁等待(六分)

当其他线程获得锁之后,等待固定时间之后,还没有获得锁就不在争夺锁。

  1. 线程中断(一分)

三、开发难度

特性支持

synchronized

StampedLock

ReentrantLock

ReentrantReadWriteLock

锁升级

支持

支持

不支持

不支持

重入

支持

不支持

可支持

可支持

读写

不支持

支持

不支持

支持

公平

不支持

不支持

支持

支持

自动释放

支持

不支持

不支持

不支持

锁等待超时

不支持

支持

支持

支持

线程中断

不支持

支持

支持

支持

实际使用

简单首选

不建议使用

比较简单

小心使用

开发人员使用分类

synchronized

StampedLock

ReentrantLock

ReentrantReadWriteLock

自研

初级开发人员

使用

不使用

不使用

不使用

不使用

中级开发人员

使用

不使用

不支持

不使用

不使用

高级开发人员

使用

不使用

不支持

不使用

不使用

研发人员

使用

不使用

使用

使用

使用

资深研发人员

使用

使用

使用

使用

使用

锁应用环境

synchronized

StampedLock

ReentrantLock

ReentrantReadWriteLock

自研

业务系

使用

不使用

不使用

不使用

不使用

功能系

使用

不使用

使用

不使用

不使用

软件系

使用

不使用

使用

使用

使用

业务模块基本不需要关注锁,需要锁的地方都应该使用synchronized,高级以下程序员都使用的数据库锁,或者其他库锁。打杂的目前在业务系没有用过java锁, 功能模块的开发与维护基本是高级或者研发人员,用到锁的地方不多,绝对大部分是使用的synchronizedReentrantLock,在spring的代码里面只见到许多synchronized,ReentrantLock还没见过 软件系比如netty,dubbo,大量使用ReentrantLock,一些独立的服务比如rocketmq,核心业务重写了ReentrantLock,还有设计自己的加锁机制

读写类型

synchronized

StampedLock

ReentrantLock

ReentrantReadWriteLock

自研

读多

不使用

不使用

不使用

使用

不使用

各半

使用

不使用

不使用

使用

不使用

写多

使用

不使用

使用

不使用

不使用

全写

使用

不使用

使用

不使用

不使用

全写操作目前发现最多的就是计数器,计数器建议使用jdk8的LongAdders(计数器),性能超级好。注意任何计数器无法保证绝对的精确性。

ReentrantLock与ReentrantReadWriteLock的写性能一样。

总结
如果要对特性重要进行排序,要排除对性能极限要求的情况,可以得到以下结论:

重入>锁升级>自动释放>锁等待超时>公平>读写>线程中断

  1. 在繁多,复杂的方法,代码,逻辑之间相互调用。谁也不知道,哪个方法,哪段代码使用了锁,一不小心死锁。所以重入是最重要的一点。 除非资深研发人员否则其他人员不应该使用StampedLock
  2. 锁升级可以做基本性能方面优化,就交给锁了,可以让锁性能在个个场景都可以保持较好的状态,从而减少锁开发与维护的工作量
  3. 自动释放对初级,中级或者高级开发来说,是一个避免出现锁问题的利器,保障开发简单,顺利。不用担心哪里忘记释放锁,从而造成锁问题
  4. 锁等待超时是防止无限锁等待而造成线程资源无限占用与线程池无线程可用的情况,从而让应用无法提供服务。是高可用服务保障的利器
  5. 复杂的环境下,不知道哪个方法,哪个代码使用了读锁还是写锁。太多未知与细节,十分头疼,需要大量的时间与精力处理读写关系,得不偿失。
做了这么多年开发与研发。感觉性能较好的情况下,不出问题与开发维护方便应该放在对性能高度最求的前面。尤其是线上问题,应该避免出现。
从上面的对比分析,synchronized的得分与评价是最高的,ReentrantLock其次, 不建议使用ReentrantReadWriteLock,禁止使用StampedLock。

四、synchronized详析

  1. synchronized 是java关键字,jvm内部实现。所以jvm可以对synchronized进行优化
  2. 每个jdk版本synchronized性能不一样,版本越高的性能越好。jdk1.6与jdk1.7之间的性能差距十分大。
  3. synchronized操作简单,jvm自动优化性能
synchronized详析锁的方式
代码语言:javascript复制
public class SynchronizedLockMode {    private static int increment = 0;    
   private Object object = new Object( );    
   public synchronized void lockMethod(){
       print("lockMethod");
   }    
   public synchronized static void  lockStaticMethod(){
       print("lockStaticMethod");
   }    
   public void lockBlock(){        synchronized( object ){
           print("lockBlock");
       }
   }
}

运行代码:

代码语言:javascript复制
@Test
   public void synchronizedLockModeTest(){
       SynchronizedLockMode   slm = new SynchronizedLockMode();
       Thread thread1 = new Thread( new Runnable( ) {            public void run ( ) {
               slm.lockMethod( );
           }
       } );
       Thread thread2 = new Thread( new Runnable( ) {            public void run ( ) {    
               SynchronizedLockMode.lockStaticMethod( );
           }
       } );
       Thread thread3 = new Thread( new Runnable( ) {            public void run ( ) {    
               slm.lockBlock( );
           }
       } );
       thread1.start( );
       thread2.start( );
       thread3.start( );        try {
           Thread.sleep( 1000 );
       } catch ( InterruptedException e ) {           e.printStackTrace();
       }
}

运行结果:

代码语言:javascript复制
lockMethod:  0  for num0
lockBlock:  1  for num0
lockStaticMethod:  0  for num0
lockBlock:  3  for num1
lockMethod:  2  for num1
lockBlock:  5  for num2
lockStaticMethod:  4  for num1
lockBlock:  7  for num3
lockMethod:  6  for num2
lockBlock:  9  for num4
lockStaticMethod:  8  for num2
lockBlock:  11  for num5
lockMethod:  10  for num3
lockBlock:  13  for num6
lockStaticMethod:  12  for num3
lockBlock:  15  for num7
lockBlock:  17  for num8
lockMethod:  14  for num4
lockBlock:  18  for num9
lockBlock:  20  for num10
lockBlock:  21  for num11
lockStaticMethod:  16  for num4
lockBlock:  22  for num12
lockMethod:  19  for num5
lockBlock:  24  for num13
lockStaticMethod:  23  for num5
lockBlock:  26  for num14
lockMethod:  25  for num6
lockBlock:  28  for num15
lockStaticMethod:  27  for num6
lockBlock:  30  for num16
lockMethod:  29  for num7
lockBlock:  32  for num17
lockStaticMethod:  31  for num7
lockBlock:  34  for num18
lockMethod:  33  for num8
lockBlock:  36  for num19
lockStaticMethod:  35  for num8
lockStaticMethod:  38  for num9
lockStaticMethod:  39  for num10
lockStaticMethod:  40  for num11
lockStaticMethod:  41  for num12
lockStaticMethod:  42  for num13
lockMethod:  37  for num9
lockStaticMethod:  43  for num14
lockMethod:  44  for num10
lockStaticMethod:  45  for num15
lockMethod:  46  for num11
lockStaticMethod:  47  for num16
lockMethod:  48  for num12
lockStaticMethod:  49  for num17
lockMethod:  50  for num13
lockStaticMethod:  51  for num18
lockMethod:  52  for num14
lockStaticMethod:  53  for num19
lockMethod:  54  for num15
lockMethod:  55  for num16
lockMethod:  56  for num17
lockMethod:  57  for num18
lockMethod:  58  for num19
代码语言:javascript复制
从上面的执行可以是否发现一个问题,答应是乱序的,自增数据是乱序的。
很多人认为:绝对是java设计的失误...使用一个图片来逻辑推理下:
  1. java与jvm绝对没有错
  2. synchronized是上锁,这点绝对没有问题
  3. 那synchronized锁了什么了?

这是我们讨论的论题,也是一个容易犯错的问题。

演示代码,有四个方法。

代码语言:javascript复制
public synchronized void lockThisObject(){
       sleep("synchronized method");
   }    
   public void VerificationLockMethodIsWhatObject(){        synchronized( this ){
           sleep("synchronized block lock this" , false);
       }
   }       public synchronized static void  lockClassObject(){
       sleep("synchronized method static ");
   }    
   public void VerificationLockStaticMethodIsWhatObject(){        synchronized( SynchronizedLockMode.class ){
           sleep("synchronized block lock SynchronizedLockMode.class" , false);
       }
   }    
   private static void sleep(String lock , boolean boo){        if(boo){
           sleep( lock );
       }else{
           System.out.println( lock   "  execute" ) ;
       }
   }    
   private static void sleep(String lock){
       sleep0( lock );
   }    
   private static void sleep0(String lock){        try {
           System.out.println( lock   "  start sleep" ) ;
           Thread.sleep( 10000 );
           System.out.println( lock   "  end sleep" ) ;
       } catch ( InterruptedException e ) {           e.printStackTrace();
       }
}

代码解读   

有四个方法分别是静态方法,非静态方法,两个方法里面有synchronized block。四个方法分别组合,测试方法的互斥行。输出内容是按照调用方法的循序执行的,synchronized block方法的输出结果在synchronized 方法之后,那么表示两个方法是互斥的。组合:

  1. 锁静态方法 块锁锁住this;
  2. 锁静态方法 块锁锁住Class;
  3. 锁非静态方法 块锁锁住this ;
  4. 锁非静态方法 块锁锁住Class。

test代码与结果 第一个组合

代码语言:javascript复制
SynchronizedLockMode   slm = new SynchronizedLockMode();
Thread thread1 = new Thread( new Runnable( ) {            public void run ( ) {
                   SynchronizedLockMode.lockClassObject( );
           }
} );
Thread thread2 = new Thread( new Runnable( ) {            public void run ( ) {
                   slm.verificationLockMethodIsWhatObject( );
           }
           } );
System.out.println( "第一个组合 锁静态方法 块锁锁住this" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第一个组合 执行结束" ) ;第一个组合 锁静态方法 块锁锁住thissynchronized method static   start sleepsynchronized block lock this  executesynchronized method static   end sleep
第一个组合 执行结束

第二个组合:

代码语言:javascript复制
thread1 = new Thread( new Runnable( ) {        public void run ( ) {
       SynchronizedLockMode.lockClassObject( );
       }
} );
thread2 = new Thread( new Runnable( ) {        public void run ( ) {
       slm.verificationLockStaticMethodIsWhatObject( );
       }
} );
System.out.println( "第二个组合 锁静态方法 块锁锁住Class" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第二个组合 执行结束" ) ;第二个组合 锁静态方法 块锁锁住Classsynchronized method static   start sleepsynchronized method static   end sleepsynchronized block lock SynchronizedLockMode.class  execute
第二个组合 执行结束

第三个组合:

代码语言:javascript复制
thread1 = new Thread( new Runnable( ) {            public void run ( ) {
           slm.lockThisObject( );
           }
} );
thread2 = new Thread( new Runnable( ) {        public void run ( ) {
       slm.verificationLockMethodIsWhatObject( );
       }
} );
System.out.println( "第三个组合 锁非静态方法 块锁锁住this" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第三个组合 执行结束" ) ;第三个组合 锁非静态方法 块锁锁住thissynchronized method  start sleepsynchronized method  end sleepsynchronized block lock this  execute
第三个组合 执行结束

第四个组合

代码语言:javascript复制
thread1 = new Thread( new Runnable( ) {            public void run ( ) {
               slm.lockThisObject( );
           }
} );
thread2 = new Thread( new Runnable( ) {            public void run ( ) {
               slm.verificationLockStaticMethodIsWhatObject( );
           }
} );
System.out.println( "第四个组合 非锁静态方法 块锁锁住Class" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第四个组合 执行结束" ) ;第四个组合 非锁静态方法 块锁锁住Classsynchronized method  start sleepsynchronized block lock SynchronizedLockMode.class  executesynchronized method  end sleep
第四个组合 执行结束

通过结果分析会发现第二,三组合的输出结果是有序的。麻烦在看看二,三组合调用的方法 第二个组合是: 锁静态方法 块锁锁住Class 第三个组合是: 锁非静态方法 块锁锁住this

结论:

synchronized关键字标记在静态方法上是锁当前的class对象。

代码语言:javascript复制
public synchronized static void XXXXX(){
   // 锁的对象是 当前的 class
}

synchronized关键字标记在非静态方法上是锁当前的实例对象(this)

代码语言:javascript复制
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的优秀

公平锁与不公平锁:
  1. 在一般竞争情况下,两者的性能可以理解为相等
  2. 在极高竞争下,不公平锁的性能是可能是公平锁的十几倍
ReentrantReadWriteLock 死锁现象
背景
代码语言:javascript复制
代码语言:javascript复制
某个深夜,老胡在看ReentrantReadWriteLock源码,想用ReentrantReadWriteLock代替ReentrantLock提高性能,反复的看调用流程与实现细节(看了两个多小时),脑海慢慢呈现整个调用流程与实现细节的流程与逻辑图,发现不对劲啊,可能一不小心出现死锁
代码语言:javascript复制
public void reentrantReadWriteLock() throws InterruptedException{
       ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
       ReadLock  readLock  = rrwl.readLock( );
       WriteLock wrtieLock = rrwl.writeLock( );       readLock.lock( );
       readLock.lock( );
       readLock.unlock( );
       readLock.unlock( );       wrtieLock.lock( );
       wrtieLock.lock( );
       wrtieLock.unlock( );
       wrtieLock.unlock( );       wrtieLock.lock( );
       readLock.lock( );
       wrtieLock.unlock( );
       readLock.unlock( );       readLock.lock( );       wrtieLock.lockInterruptibly( );
       readLock.unlock( );
       wrtieLock.unlock( );
   }
代码语言:javascript复制
经过测试之后,老胡出了一点冷汗,这个死锁隐藏得太深了。还好是老胡慢慢,慢,看出来了,以老胡的编码方式,还真得出现这样的死锁

ReentrantLock 与 ReentrantReadWriteLock 在高并发下的不公平锁 出现饿死现象:

代码语言:javascript复制
在发现死锁现象同一个深夜,老胡在仔细反复的看公布与不公平,读写锁的细节。反复的看调用流程与实现细节,一边准备与周公喝茶了,一个低头砸到桌子上,脑海里整个调用流程与实现细节的流程与逻辑图砸出一个闪光,找到一个问题
在高并发下,很多线程争夺一个锁的时候,在队列的里面的锁可能能难争夺到锁,争夺不到,会饿死啊。

锁方式

ReentrantLock

ReentrantReadWriteLock

lock

饥饿

饥饿

lockInterruptibly

饥饿

饥饿

tryLock

不饥饿

不饥饿

tryLock(超时)

超时饿醒

超时饿醒

在高并发情况下,使用tryLock(超时)杜绝 饥饿。没获得锁,可以直接异常与返回异常结果

锁使用总结:

  1. 能不使用锁,绝对不要使用...........
  2. 注意上面说的细节,比如synchronized锁的对象等
  3. 优先使用synchronized关键字,能不用使用synchronized块就不使用
  4. 高并发的情况下使用synchronized,麻烦关闭偏向锁-XX:-UseBiasedLocking
  5. 减少锁粒度
  1. 优先使用重入锁,禁止非重入锁StampedLock的使用
  2. 一定要分析场景,在选择对应的锁,如果不分析只能使用synchronized
  3. tryLock(超时) 是处理死锁与饥饿的神器。
  4. 一个class或者一个实例里面只允许一个锁。两个锁容易出现死锁。这个锁必须能重入
代码语言:javascript复制
private Object object = new Object();public void a(){
   synchronized(object){   }
}
public void b(){
   synchronized(object){   }
}
// c 方法与a,b方法的锁不是一个,在这个类里面有两个锁分别是 object与this,
public void c(){
   synchronized(this){   }
}
代码语言:javascript复制
// 同时存在 a b锁,很容易不小心死锁。
ReentrantLock a= new ReentrantLock();
ReentrantLock b= new ReentrantLock();

至此 2018年01月25日00.26. 历经一个多月,才写完。也真的佩服文字表达能力与技术描述能力强的人,1个多月啊,欢迎大家交流,拍砖。

0 人点赞