synchronized的锁优化是怎样的?

admin2024-05-15  0

一、问题解析

高效并发是从JDK 1.5 到 JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本中花费了很大的精力去对Java中的锁进行优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题。自旋锁

我们介绍的synchronized的实现方式时,说过,它是使用Monitor进行加锁,这是一种互斥锁,为了表示他对性能的影响我们称之为重量级锁。

这种互斥锁在互斥同步上对性能的影响很大,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。

就像去银行办业务的例子,当你来到银行,发现柜台前面都有人的时候,你需要取一个号,然后再去等待区等待,一直等待被叫号。这个过程是比较浪费时间的,那么有没有什么办法改进呢?

有一种比较好的设计,那就是银行提供自动取款机,当你去银行取款的时候,你不需要取号,不需要去休息区等待叫号,你只需要找到一台取款机,排在其他人后面等待取款就行了。


之所以能这样做,是因为取款的这个过程相比较之下是比较节省时间的。如果所有人去银行都只取款,或者办理业务的时间都很短的话,那也就可以不需要取号,不需要去单独的休息区,不需要听叫号,也不需要再跑到对应的柜台了。

而,在程序中,Java虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。

自旋锁在JDK 1.4中已经引入,在JDK 1.6中默认开启。

很多人在对于自旋锁的概念不清楚的时候可能会有以下疑问:这么听上去,自旋锁好像和阻塞锁没啥区别,反正都是等着嘛。

● 对于去银行取钱的你来说,站在取款机面前等待和去休息区等待叫号有一个很大的区别:
○ 那就是如果你在休息区等待,这段时间你什么都不需要管,随意做自己的事情,等着被唤醒就行了。
○ 如果你在取款机面前等待,那么你需要时刻关注自己前面还有没有人,因为没人会唤醒你。
○ 很明显,这种直接去取款机前面排队取款的效率是比较高。

所以呢,自旋锁和阻塞锁最大的区别就是,到底要不要放弃处理器的执行时间。对于阻塞锁和自旋锁来说,都是要等待获得共享资源。但是阻塞锁是放弃了CPU时间,进入了等待区,等待被唤醒。而自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间短。适合使用自旋锁。

锁消除

除了自旋锁之后,JDK中还有一种锁的优化被称之为锁消除。还拿去银行取钱的例子说。

你去银行取钱,所有情况下都需要取号,并且等待吗?其实是不用的,当银行办理业务的人不多的时候,可能根本不需要取号,直接走到柜台前面办理业务就好了。 

能这么做的前提是,没有人和你抢着办业务。

上面的这种例子,在锁优化中被称作“锁消除”,是JIT编译器对内部锁的具体实现所做的一种优化。

在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。

如以下代码:

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:
 

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

了解我的朋友都知道,一般到这个时候,我就会开始反编译,然后拿出反编译之后的代码来证明锁优化确实存在。

但是,之前很多例子之所以可以用反编译工具,是因为那些“优化”,如语法糖等,是在javac编译阶段发生的,并不是在JIT编译阶段发生的。而锁优化,是JIT编译器的功能,所以,无法使用现有的反编译工具查看具体的优化结果。(关于javac编译和JIT编译的关系和区别,我在我的知识星球中单独发了一篇文章介绍。)
 

总之,读者只需要知道,在使用synchronized的时候,如果JIT经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除。

锁粗化

很多人都知道,在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。

这也是很多人愿意使用同步代码块来代替同步方法的原因,因为往往他的粒度会更小一些,这其实是很有道理的。

还是我们去银行柜台办业务,最高效的方式是你坐在柜台前面的时候,只办和银行相关的事情。如果这个时候,你拿出手机,接打几个电话,问朋友要往哪个账户里面打钱,这就很浪费时间了。最好的做法肯定是提前准备好相关资料,在办理业务时直接办理就好了。

加锁也一样,把无关的准备工作放到锁外面,锁内部只处理和并发相关的内容。这样有助于提高效率。

那么,这和锁粗化有什么关系呢?可以说,大部分情况下,减小锁的粒度是很正确的做法,只有一种特殊的情况下,会发生一种叫做锁粗化的优化。

就像你去银行办业务,你为了减少每次办理业务的时间,你把要办的五个业务分成五次去办理,这反而适得其反了。因为这平白的增加了很多你重新取号、排队、被唤醒的时间。

如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。

当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。

如以下代码: 

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
}

会被粗化成:

synchronized(this){  
    for(int i=0;i<100000;i++){  
        do();  
}

这其实和我们要求的减小锁粒度并不冲突。减小锁粒度强调的是不要在银行柜台前做准备工作以及和办理业务无关的事情。而锁粗化建议的是,同一个人,要办理多个业务的时候,可以在同一个窗口一次性办完,而不是多次取号多次办理。

二、总结

自Java 6/Java 7开始,Java虚拟机对内部锁的实现进行了一些优化。这些优化主要包括锁消除(Lock Elision)、锁粗化(Lock Coarsening)、偏向锁(Biased Locking)以及适应性自旋锁(Adaptive Locking)。这些优化仅在Java虚拟机server模式下起作用(即运行Java程序时我们可能需要在命令行中指定Java虚拟机参数“-server”以开启这些优化)。

本文主要介绍了自旋锁、锁粗化和锁消除的概念。在JIT编译过程中,虚拟机会根据情况使用这三种技术对锁进行优化,目的是减少锁的竞争,提升性能。 

 三、粉丝福利

我是浮生,一个工作十四年经验的Java程序员!

最近很多同学问我有没有java学习资料,我根据我从小白到架构师多年的学习经验整理出来了一份80W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴 可以关注我
公众号:“
 灰灰聊架构 ”, 回复暗号:“ 321 ”即可获取

synchronized的锁优化是怎样的?,第1张

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!