Java并发编程系列:html
Java 并发编程:核心理论 java
1、重量级锁app
上篇文章中向你们介绍了Synchronized的用法及其实现的原理。如今咱们应该知道,Synchronized是经过对象内部的一个叫作监视器锁(monitor)来实现的。可是监视器锁本质又是依赖于底层的操做系统的Mutex Lock来实现的。而操做系统实现线程之间的切换这就须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何Synchronized效率低的缘由。所以,这种依赖于操做系统Mutex Lock所实现的锁咱们称之为“重量级锁”。JDK中对Synchronized作的种种优化,其核心都是为了减小这种重量级锁的使用。JDK1.6之后,为了减小得到锁和释放锁所带来的性能消耗,提升性能,引入了“轻量级锁”和“偏向锁”。jvm
2、轻量级锁 ide
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁(可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,咱们也能够经过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:性能
锁状态 |
25 bit |
4bit |
1bit |
2bit |
|
23bit |
2bit |
是不是偏向锁 |
锁标志位 |
||
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
重量级锁 |
指向互斥量(重量级锁)的指针 |
10 |
|||
GC标记 |
空 |
11 |
|||
偏向锁 |
线程ID |
Epoch |
对象分代年龄 |
1 |
01 |
无锁 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程以前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁。
一、轻量级锁的加锁过程
(1)在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(3),不然执行步骤(4)。
(4)若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
(5)若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。
图2.1 轻量级锁CAS操做以前堆栈与对象的状态
图2.2 轻量级锁CAS操做以后堆栈与对象的状态
二、轻量级锁的解锁过程:
(1)经过CAS操做尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)若是替换成功,整个同步过程就完成了。
(3)若是替换失败,说明有其余线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
3、偏向锁
引入偏向锁是为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径,由于轻量级锁的获取及释放依赖屡次CAS原子指令,而偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能。
一、偏向锁获取过程:
(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
(2)若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤(5),不然进入步骤(3)。
(3)若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行(5);若是竞争失败,执行(4)。
(4)若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
二、偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
三、重量级锁、轻量级锁和偏向锁之间转换
图 2.3三者的转换图
该图主要是对上述内容的总结,若是对上述内容有较好的了解的话,该图应该很容易看懂。
4、其余优化
一、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中咱们知道,当线程在获取轻量级锁的过程当中执行CAS操做失败时,是要经过自旋来获取重量级锁的。问题在于,自旋是须要消耗CPU的,若是一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,若是还没获取到锁就进入阻塞状态。可是JDK采用了更聪明的方式——适应性自旋,简单来讲就是线程若是自旋成功了,则下次自旋的次数会更多,若是自旋失败了,则自旋的次数就会减小。
二、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将屡次链接在一块儿的加锁、解锁操做合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
1 package com.paddx.test.string; 2 3 public class StringBufferTest { 4 StringBuffer stringBuffer = new StringBuffer(); 5 6 public void append(){ 7 stringBuffer.append("a"); 8 stringBuffer.append("b"); 9 stringBuffer.append("c"); 10 } 11 }
这里每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
三、锁消除(Lock Elimination):锁消除即删除没必要要的加锁操做。根据代码逃逸技术,若是判断到一段代码中,堆上的数据不会逃逸出当前线程,那么能够认为这段代码是线程安全的,没必要要加锁。看下面这段程序:
1 package com.paddx.test.concurrent; 2 3 public class SynchronizedTest02 { 4 5 public static void main(String[] args) { 6 SynchronizedTest02 test02 = new SynchronizedTest02(); 7 //启动预热 8 for (int i = 0; i < 10000; i++) { 9 i++; 10 } 11 long start = System.currentTimeMillis(); 12 for (int i = 0; i < 100000000; i++) { 13 test02.append("abc", "def"); 14 } 15 System.out.println("Time=" + (System.currentTimeMillis() - start)); 16 } 17 18 public void append(String str1, String str2) { 19 StringBuffer sb = new StringBuffer(); 20 sb.append(str1).append(str2); 21 } 22 }
虽然StringBuffer的append是一个同步方法,可是这段程序中的StringBuffer属于一个局部变量,而且不会从该方法中逃逸出去,因此其实这过程是线程安全的,能够将锁消除。下面是我本地执行的结果:
为了尽可能减小其余因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。经过上面程序,能够看出消除锁之后性能仍是有比较大提高的。
注:可能JDK各个版本之间执行的结果不尽相同,我这里采用的JDK版本为1.6。
5、总结
本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,可是这两种锁也不是彻底没缺点的,好比竞争比较激烈的时候,不但没法提高效率,反而会下降效率,由于多了一个锁升级的过程,这个时候就须要经过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:
锁 |
优势 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不须要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
若是线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
适用于只有一个线程访问同步块场景。 |
轻量级锁 |
竞争的线程不会阻塞,提升了程序的响应速度。 |
若是始终得不到锁竞争的线程使用自旋会消耗CPU。 |
追求响应时间。 同步块执行速度很是快。 |
重量级锁 |
线程竞争不使用自旋,不会消耗CPU。 |
线程阻塞,响应时间缓慢。 |
追求吞吐量。 同步块执行速度较长。 |
参考文献:
http://www.iteye.com/topic/1018932
http://www.infoq.com/cn/articles/java-se-16-synchronized
http://frank1234.iteye.com/blog/2163142