二手书籍交易网站开发方式,网站制作优质公司,wordpress企业模板下载,wordpress 用户登录ip转载自 深入理解多线程#xff08;五#xff09;—— Java虚拟机的锁优化技术本文是《深入理解多线程》的第五篇文章#xff0c;前面几篇文章中我们从synchronized的实现原理开始#xff0c;一直介绍到了Monitor的实现原理。这一篇在前几篇的基础上#xff0c;深入介绍一下…转载自 深入理解多线程五—— Java虚拟机的锁优化技术本文是《深入理解多线程》的第五篇文章前面几篇文章中我们从synchronized的实现原理开始一直介绍到了Monitor的实现原理。这一篇在前几篇的基础上深入介绍一下JVM中的锁优化技术。关于这部分知识点笔者翻阅了很多书籍和博文都没有找到介绍的很清楚的文章包括我正在写的后面会发出来的锁膨胀技术。笔者也是看了很多HotSpot虚拟机的源码、阅读了很多文档资料。所以希望读者们可以把这个系列文章好好看看和我一起深入理解多线程。前情提要通过前面几篇文章我们已经知道
1、同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时需要先获得锁才能执行该方法。《深入理解多线程一——Synchronized的实现原理》
2、同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。《深入理解多线程四—— Moniter的实现原理》
3、在HotSpot虚拟机中使用oop-klass模型来表示对象。每一个Java类在被JVM加载的时候JVM会给这个类创建一个instanceKlass保存在方法区用来在JVM层表示该Java类。当我们在Java代码中使用new创建一个对象的时候JVM会创建一个instanceOopDesc对象这个对象中包含了对象头以及实例数据。《深入理解多线程二—— Java的对象模型》
4、对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。对象的状态一共有五种分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。《深入理解多线程三—— Java的对象头》
在上一篇文章的最后我们说过事实上只有在JDK1.6之前synchronized的实现才会直接调用ObjectMonitor的enter和exit这种锁被称之为重量级锁。
高效并发是从JDK 1.5 到 JDK 1.6的一个重要改进HotSpot虚拟机开发团队在这个版本中花费了很大的精力去对Java中的锁进行优化如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术都是为了在线程之间更高效的共享数据以及解决竞争问题。
本文主要先来介绍一下自旋、锁消除以及锁粗化等技术。
这里简单说明一下本文要介绍的这几个概念以及后面要介绍的轻量级锁和偏向锁其实对于使用他的开发者来说是屏蔽掉了的也就是说作为一个Java开发你只需要知道你想在加锁的时候使用synchronized就可以了具体的锁的优化是虚拟机根据竞争情况自行决定的。
也就是说在JDK 1.5 以后我们即将介绍的这些概念都被封装在synchronized中了。线程状态要想把锁说清楚一个重要的概念不得不提那就是线程和线程的状态。锁和线程的关系是怎样的呢举个简单的例子你就明白了。
比如你今天要去银行办业务你到了银行之后要先取一个号然后你坐在休息区等待叫号过段时间广播叫到你的号码之后会告诉你去哪个柜台办理业务这时你拿着你手里的号码去到对应的柜台找相应的柜员开始办理业务。当你办理业务的时候这个柜台和柜台后面的柜员只能为你自己服务。当你办完业务离开之后广播再喊其他的顾客前来办理业务。这个例子中每个顾客是一个线程。
柜台前面的那把椅子就是锁。
柜台后面的柜员就是共享资源。
你发现无法直接办理业务要取号等待的过程叫做阻塞。
当你听到叫你的号码的时候你起身去办业务这就是唤醒。
当你坐在椅子上开始办理业务的时候你就获得锁。
当你办完业务离开的时候你就释放锁。对于线程来说一共有五种状态分别为初始状态(New) 、就绪状态(Runnable) 、运行状态(Running) 、阻塞状态(Blocked) 和死亡状态(Dead) 。自
旋
锁
在前一篇文章中我们介绍的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);}这里可能有读者会质疑了代码是程序员自己写的程序员难道没有能力判断要不要加锁吗就像以上代码完全没必要加锁有经验的开发者一眼就能看的出来的。其实道理是这样但是还是有可能有疏忽比如我们经常在代码中使用StringBuffer作为局部变量而StringBuffer中的append是线程安全的有synchronized修饰的这种情况开发者可能会忽略。这时候JIT就可以帮忙优化进行锁消除。
了解我的朋友都知道一般到这个时候我就会开始反编译然后拿出反编译之后的代码来证明锁优化确实存在。
但是之前很多例子之所以可以用反编译工具是因为那些“优化”如语法糖等是在javac编译阶段发生的并不是在JIT编译阶段发生的。而锁优化是JIT编译器的功能所以无法使用现有的反编译工具查看具体的优化结果。关于javac编译和JIT编译的关系和区别我在我的知识星球中单独发了一篇文章介绍。
但是如果读者感兴趣还是可以看的只是会复杂一点首先你要自己build一个fasttest版本的jdk然后在使用java命令对.class文件进行执行的时候加上-XX:PrintEliminateLocks参数。而且jdk的模式还必须是server模式。
总之读者只需要知道在使用synchronized的时候如果JIT经过逃逸分析之后发现并无线程安全问题的话就会做锁消除。锁粗化
很多人都知道在代码中需要加锁的时候我们提倡尽量减小锁的粒度这样可以避免不必要的阻塞。
这也是很多人原因是用同步代码块来代替同步方法的原因因为往往他的粒度会更小一些这其实是很有道理的。
还是我们去银行柜台办业务最高效的方式是你坐在柜台前面的时候只办和银行相关的事情。如果这个时候你拿出手机接打几个电话问朋友要往哪个账户里面打钱这就很浪费时间了。最好的做法肯定是提前准备好相关资料在办理业务时直接办理就好了。加锁也一样把无关的准备工作放到锁外面锁内部只处理和并发相关的内容。这样有助于提高效率。
那么这和锁粗化有什么关系呢可以说大部分情况下减小锁的粒度是很正确的做法只有一种特殊的情况下会发生一种叫做锁粗化的优化。
就像你去银行办业务你为了减少每次办理业务的时间你把要办的五个业务分成五次去办理这反而适得其反了。因为这平白的增加了很多你重新取号、排队、被唤醒的时间。
如果在一段代码中连续的对同一个对象反复加锁解锁其实是相对耗费资源的这种情况可以适当放宽加锁的范围减少性能消耗。
当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁甚至加锁操作出现在循环体中的时候会将加锁同步的范围扩散粗化到整个操作序列的外部。
如以下代码
for(int i0;i100000;i){ synchronized(this){ do(); }
会被粗化成
synchronized(this){ for(int i0;i100000;i){ do(); }
这其实和我们要求的减小锁粒度并不冲突。减小锁粒度强调的是不要在银行柜台前做准备工作以及和办理业务无关的事情。而锁粗化建议的是同一个人要办理多个业务的时候可以在同一个窗口一次性办完而不是多次取号多次办理。总结自Java 6/Java 7开始Java虚拟机对内部锁的实现进行了一些优化。这些优化主要包括锁消除Lock Elision、锁粗化Lock Coarsening、偏向锁Biased Locking以及适应性自旋锁Adaptive Locking。这些优化仅在Java虚拟机server模式下起作用即运行Java程序时我们可能需要在命令行中指定Java虚拟机参数“-server”以开启这些优化。
本文主要介绍了自旋锁、锁粗化和锁消除的概念。在JIT编译过程中虚拟机会根据情况使用这三种技术对锁进行优化目的是减少锁的竞争提升性能。
当你来到银行办理业务的时候你想取钱银行工作人员了解到你要取钱之后让你你直接站在取款机前面排队等待并且告诉你自己时刻关注前面的排队状况。这就叫自旋。
当你来到银行办理业务的时候银行工作人员告诉你由于现在办理业务的人很少让你直接到1号柜台去办理业务。这就叫锁消除。
当你来到银行办理业务的时候你取了10个号准备进行十次排队进行转账银行工作人员了解情况之后但是你在一次办理业务过程中进行了10次转账办理了所有业务。这就叫锁粗化。