深刻理解多线程(五)—— Java虚拟机的锁优化技术

本文是《深刻理解多线程》的第五篇文章,前面几篇文章中咱们从synchronized的实现原理开始,一直介绍到了Monitor的实现原理。java

前情提要

经过前面几篇文章,咱们已经知道:程序员

一、同步方法经过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,须要先得到锁才能执行该方法。《深刻理解多线程(一)——Synchronized的实现原理安全

二、同步代码块经过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先得到所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。《深刻理解多线程(四)—— Moniter的实现原理多线程

三、在HotSpot虚拟机中,使用oop-klass模型来表示对象。每个Java类,在被JVM加载的时候,JVM会给这个类建立一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当咱们在Java代码中,使用new建立一个对象的时候,JVM会建立一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。《深刻理解多线程(二)—— Java的对象模型并发

四、对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。《深刻理解多线程(三)—— Java的对象头app

在上一篇文章的最后,咱们说过,事实上,只有在JDK1.6以前,synchronized的实现才会直接调用ObjectMonitorenterexit,这种锁被称之为重量级锁。工具

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

本文,主要先来介绍一下自旋、锁消除以及锁粗化等技术。性能

这里简单说明一下,本文要介绍的这几个概念,以及后面要介绍的轻量级锁和偏向锁,其实对于使用他的开发者来讲是屏蔽掉了的,也就是说,做为一个Java开发,你只须要知道你想在加锁的时候使用synchronized就能够了,具体的锁的优化是虚拟机根据竞争状况自行决定的。优化

也就是说,在JDK 1.5 之后,咱们即将介绍的这些概念,都被封装在synchronized中了。

线程状态

要想把锁说清楚,一个重要的概念不得不提,那就是线程和线程的状态。锁和线程的关系是怎样的呢,举个简单的例子你就明白了。

好比,你今天要去银行办业务,你到了银行以后,要先取一个号,而后你坐在休息区等待叫号,过段时间,广播叫到你的号码以后,会告诉你去哪一个柜台办理业务,这时,你拿着你手里的号码,去到对应的柜台,找相应的柜员开始办理业务。当你办理业务的时候,这个柜台和柜台后面的柜员只能为你本身服务。当你办完业务离开以后,广播再喊其余的顾客前来办理业务。

Pic1

这个例子中,每一个顾客是一个线程。 柜台前面的那把椅子,就是。 柜台后面的柜员,就是共享资源。 你发现没法直接办理业务,要取号等待的过程叫作阻塞。 当你听到叫你的号码的时候,你起身去办业务,这就是唤醒。 当你坐在椅子上开始办理业务的时候,你就得到锁。 当你办完业务离开的时候,你就释放锁

对于线程来讲,一共有五种状态,分别为:初始状态(New) 、就绪状态(Runnable) 、运行状态(Running) 、阻塞状态(Blocked) 和死亡状态(Dead) 。

thread

自旋锁

前一篇文章中,咱们介绍的synchronized的实现方式中使用Monitor进行加锁,这是一种互斥锁,为了表示他对性能的影响咱们称之为重量级锁。

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

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

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

Pic2

之因此能这样作,是由于取款的这个过程相比较之下是比较节省时间的。若是全部人去银行都只取款,或者办理业务的时间都很短的话,那也就能够不须要取号,不须要去单独的休息区,不须要听叫号,也不须要再跑到对应的柜台了。

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

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

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

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

  • 对于去银行取钱的你来讲,站在取款机面前等待和去休息区等待叫号有一个很大的区别:

    • 那就是若是你在休息区等待,这段时间你什么都不须要管,随意作本身的事情,等着被唤醒就好了。

    • 若是你在取款机面前等待,那么你须要时刻关注本身前面还有没有人,由于没人会唤醒你。

    • 很明显,这种直接去取款机前面排队取款的效率是比较高。

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

因为自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,因此响应速度更快。但当线程数不停增长时,性能降低明显,由于每一个线程都须要执行,占用CPU时间。若是线程竞争不激烈,而且保持锁的时间段。适合使用自旋锁。

锁消除

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

你去银行取钱,全部状况下都须要取号,而且等待吗?实际上是不用的,当银行办理业务的人很少的时候,可能根本不须要取号,直接走到柜台前面办理业务就行了。

Pic3

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

上面的这种例子,在锁优化中被称做“锁消除”,是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);
}
复制代码

这里,可能有读者会质疑了,代码是程序员本身写的,程序员难道没有能力判断要不要加锁吗?就像以上代码,彻底不必加锁,有经验的开发者一眼就能看的出来的。其实道理是这样,可是仍是有可能有疏忽,好比咱们常常在代码中使用StringBuffer做为局部变量,而StringBuffer中的append是线程安全的,有synchronized修饰的,这种状况开发者可能会忽略。这时候,JIT就能够帮忙优化,进行锁消除。

了解个人朋友都知道,通常到这个时候,我就会开始反编译,而后拿出反编译以后的代码来证实锁优化确实存在。

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

可是,若是读者感兴趣,仍是能够看的,只是会复杂一点,首先你要本身build一个fasttest版本的jdk,而后在使用java命令对.class文件进行执行的时候加上-XX:+PrintEliminateLocks参数。并且jdk的模式还必须是server模式。

总之,读者只须要知道,在使用synchronized的时候,若是JIT通过逃逸分析以后发现并没有线程安全问题的话,就会作锁消除。

锁粗化

不少人都知道,在代码中,须要加锁的时候,咱们提倡尽可能减少锁的粒度,这样能够避免没必要要的阻塞。

这也是不少人缘由是用同步代码块来代替同步方法的缘由,由于每每他的粒度会更小一些,这实际上是颇有道理的。

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

Pic4

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

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

就像你去银行办业务,你为了减小每次办理业务的时间,你把要办的五个业务分红五次去办理,这反而拔苗助长了。由于这平白的增长了不少你从新取号、排队、被唤醒的时间。

若是在一段代码中连续的对同一个对象反复加锁解锁,实际上是相对耗费资源的,这种状况能够适当放宽加锁的范围,减小性能消耗。

当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编译过程当中,虚拟机会根据状况使用这三种技术对锁进行优化,目的是减小锁的竞争,提高性能。

相关文章
相关标签/搜索