Java锁细节整理

导读

作者:老胡(微信号: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. 线程中断(一分)

三、开发难度

特性支持

synchronizedStampedLockReentrantLock

Reentrant

ReadWriteLock

锁升级支持支持不支持不支持
重入支持不支持可支持可支持
读写不支持支持不支持支持
公平不支持不支持支持支持
自动释放支持不支持不支持不支持
锁等待超时不支持支持支持支持
线程中断不支持支持支持支持
实际使用简单首选不建议使用比较简单小心使用
开发人员使用分类

synchronizedStampedLockReentrantLock

Reentrant

ReadWriteLock

自研
初级开发人员使用不使用不使用不使用不使用
中级开发人员使用不使用不支持不使用不使用
高级开发人员使用不使用不支持不使用不使用
研发人员使用不使用使用使用使用
资深研发人员使用使用使用使用使用
锁应用环境



synchronizedStampedLockReentrantLock

Reentrant

ReadWriteLock

自研
业务系使用不使用不使用不使用不使用
功能系使用不使用使用不使用不使用
软件系使用不使用使用使用使用


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


读写类型



synchronizedStampedLockReentrantLock

Reentrant

ReadWriteLock

自研
读多不使用不使用不使用使用不使用
各半使用不使用不使用使用不使用
写多使用不使用使用不使用不使用
全写使用不使用使用不使用

不使



全写操作目前发现最多的就是计数器,计数器建议使用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详析锁的方式
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");
        }
    }
}

运行代码:

@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();
        }
}

运行结果:

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

从上面的执行可以是否发现一个问题,答应是乱序的,自增数据是乱序的。
很多人认为:绝对是java设计的失误...使用一个图片来逻辑推理下:

  1. java与jvm绝对没有错

  2. synchronized是上锁,这点绝对没有问题

  3. 那synchronized锁了什么了?

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


演示代码,有四个方法。

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代码与结果 第一个组合

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
第一个组合 执行结束

第二个组合:

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
第二个组合 执行结束

第三个组合:

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
第三个组合 执行结束

第四个组合

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对象。

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的优秀

公平锁与不公平锁:
  1. 在一般竞争情况下,两者的性能可以理解为相等

  2. 在极高竞争下,不公平锁的性能是可能是公平锁的十几倍

ReentrantReadWriteLock 死锁现象
背景
某个深夜,老胡在看ReentrantReadWriteLock源码,想用ReentrantReadWriteLock代替ReentrantLock提高性能,反复的看调用流程与实现细节(看了两个多小时),脑海慢慢呈现整个调用流程与实现细节的流程与逻辑图,发现不对劲啊,可能一不小心出现死锁
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( );
    }
经过测试之后,老胡出了一点冷汗,这个死锁隐藏得太深了。还好是老胡慢慢,慢,看出来了,以老胡的编码方式,还真得出现这样的死锁

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

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

Reentrant

ReadWriteLock

lock饥饿饥饿
lockInterruptibly饥饿饥饿
tryLock不饥饿不饥饿
tryLock(超时)超时饿醒超时饿醒

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

锁使用总结:

  1. 能不使用锁,绝对不要使用...........

  2. 注意上面说的细节,比如synchronized锁的对象等

  3. 优先使用synchronized关键字,能不用使用synchronized块就不使用

  4. 高并发的情况下使用synchronized,麻烦关闭偏向锁-XX:-UseBiasedLocking

  5. 减少锁粒度

  6. 优先使用重入锁,禁止非重入锁StampedLock的使用

  7. 一定要分析场景,在选择对应的锁,如果不分析只能使用synchronized

  8. tryLock(超时) 是处理死锁与饥饿的神器。

  9. 一个class或者一个实例里面只允许一个锁。两个锁容易出现死锁。这个锁必须能重入

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){

    }
}
// 同时存在 a b锁,很容易不小心死锁。
ReentrantLock a= new ReentrantLock();
ReentrantLock b= new ReentrantLock();

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


扫码关注老胡个人公众号"老胡杂物房”



扫码加入知数堂技术交流QQ群

(群号:579036588)

群内可@各位助教了解更多课程信息




知数堂

叶金荣与吴炳锡联合打造

领跑IT精英培训

行业资深专家强强联合,倾心定制

MySQL实战/MySQL优化 /大数据实战/ Python/ SQL优化

数门精品课程

紧随技术发展趋势,定期优化培训教案

融入大量生产案例,贴合企业一线需求

社群陪伴学习,一次报名,可学3期

DBA、开发工程师必修课

上千位学员已华丽转身,薪资翻番,职位提升

改变已悄然发生,你还在等什么?

扫码下载知数堂精品课程试听视频

(MySQL 实战/优化、大数据实战、Python开发,及SQL优化等课程)

密码:hg3h




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值