我们在多线程编程的过程中,对于锁的使用有一些可以优化的地方。下面介绍一下,自己在写业务代码中需要注意的问题和JVM对锁的优化操作。
业务代码中的注意事项
减少锁的持有时间:意思就是在同步快内部的代码,对于不需要同步控制的代码片段,尽量不要写在同步快内部。
减小锁粒度:这个可以用JDK中的ConcurrentHashMap作为类比,将原先的对整个Map加锁,调整为对每个片段加锁。
读写分离替换独占锁:也就是在有读有写的场景下(最好是读大于写),使用ReadWriteLock替换ReentrantLock,从而提高性能。
锁分离:这个可以使用JDK中的BlockingQueue来类比,在操作队列的过程中,分别对队列两端做同步控制,从而使出队/入队两类操作分离。
锁粗化:当同一个监视器对象的2个同步块,并且同步块之间只有很少的代码片段(耗时短),这个时候可以将2个同步块合并为1个同步块,从而减少线程上下文切换的开销。
JVM的锁优化
java中的锁,存放在监视器对象的对象头的Mark Word里面。在32位的虚拟机中,一个字宽等于4个字节(32bit)。如果对象类型为数组,则用3个字宽(Word)表示对象头;如果类型为非数组,则使用2个字宽表示。如下图所示:
在程序运行的过程中,对象头中的Mark Word可能有以下情况:
偏向锁
java1.6中为了减少获得锁和释放锁带来的性能损耗,引入了“偏向锁”和“轻量级锁”。所以JDK1.6里锁一共有4中状态:无锁、偏向锁、轻量级锁和重量级锁,锁会随着竞争的不断加剧而逐渐升级。(锁一旦升级就不能降级,这也是为了提高效率)
所谓偏向锁,就是锁会偏向于已经占有锁的线程,这种锁容易出现在没有竞争的情况下。在没有竞争的情况下,当获得锁的线程再次进入同步块时,不需要做同步处理。当其他线程请求锁的时候,该锁的偏向模式结束。JVM使用:-XX:+UseBiasedLocking参数启用偏向锁(1.6以后默认启用)。但是需要注意一点的是:在竞争激烈的场景下,开启偏向锁会增加系统的负担。
偏向锁在存在竞争时,首先会撤销偏向模式,转为无锁状态(不可偏向),然后再升级为轻量级锁:
轻量级锁
当偏向锁失效时,线程间会竞争轻量级锁。此时会使用CAS修改对象头的Mark Word,如果成功获得锁,则执行同步块。若获取不到,自旋操作后,升级为重量级锁。
自旋锁
当线程竞争轻量级锁失败后,此时并不会立即在操作系统层面挂起,而是做一些空循环,也就是所谓的自旋锁。系统希望在自旋的过程中可以获得锁。如果若干次之后还未获得到,则进入阻塞状态,加重量级锁。
重量级锁
这里就是指上面锁竞争的最后一道关卡,进入了线程挂起的状态。此时如果获取不到锁,线程就不走了。
由轻量级锁膨胀为重量级锁的过程如下所示:
从以上两个流程看来,如果打开了偏向锁,首先会使用偏向锁;如果偏向锁失败,等待偏向锁撤销后(其他线程撤销),加轻量级锁;如果轻量级锁业竞争失败,那么此时会进入到自旋锁操作;自旋超时之后,则进入重量级锁,在操作系统层面挂起等待。
锁消除
锁消除相关联的一项技术就是逃逸分析。就是分析某一个变量会不会在一个作用域外部引用。例如我们在方法内部的变量中,使用了StringBuffer sb处理了字符串。那么在实际的执行过程中,虚拟机会消除StringBuffer内部的同步控制,意思就是这个过程不需要加锁。还有一点需要注意的是String str = a + b; 这种字符串拼接代码,在JDK1.5以前会转化为StringBuffer的append()处理;在1.5及以后版本,会转化为StringBuilder的append()处理。
可以使用-XX:+DoEscapeAnalysis打开逃逸分析;使用-XX:+EliminateLocks打开锁消除
参考:《深入理解java虚拟机》 http://www.importnew.com/21933.html