一.synchronized升级过程
在最开始JDK1.0,1.2的时候,synchronized就是重量级锁,后来JDK对synchronized进行了一系列优化,这个优化就是有个升级过程。这个升级过程被markword清晰地进行了记录。
整个锁升级的过程大概分为:
new-->偏向锁-->轻量级锁(无锁,自旋锁,自适应自旋)-->重量级锁
我们结合下面这张图具体谈一下
从刚刚new出来的时候,首先上的是偏向锁,出现争用,升级为轻量级锁,竞争状态变得特备激烈,变为重量级锁。这些信息全部记录在markword的8个字节中。这张图也说明了在不同的状态下,这64位到底是什么样的布局。在无锁态,头三位代表一个没有锁的状态。如果升级为偏向锁,也是头三位代表它为偏向锁。如果再升级为更重量级的锁,只用两位代表就行了。
刚new出来时,我们看下这64位都对应什么东西?
分代年龄占4位,其最大值即为15。这也就是我们所说的当分代年龄达到最大值时,就从年轻代变为老年代。
hashcode占31位,准确来说,对hashcode方法进行调用之后,它才会把这个值存进来,没有进行调用,这里面是没有值的。
我们看下调用前后内存布局变化:
下面再谈下偏向锁,第一个拥有锁对象的线程,先不上那么重量级的锁,先给它贴个标签, 类比于在卫生间门上贴张写有名字的纸。偏向锁的意思就是它偏向于第一个刚刚进来的线程,因为在竞争不是那么激烈的情况下,其实一个线程拥有这把锁之后,很快就结束了,干完事之后就将这把锁释放了,所以没有必要申请重量级的锁,干脆贴个名字就完事了。所谓的往上贴个标签指的是在markword里面用54位记录了指向当前线程的指针。概括下,从new出来对象到上偏向锁,就是第一个线程来占位的时候,把自己的线程id往上一贴,这坑就属于它了。
当出现第二个线程的时候,就升级为轻量级锁。当B线程来抢坑的时候,发现门上已经贴有A线程的ID了,那A、B两个线程就开始抢坑了,抢坑的过程为:首先撤销偏向锁状态,即把门上标签撕了,每个线程都有自己的线程栈了,在各自的线程栈里生成自己的一个对象,这个对象叫做LockRecord(锁记录)。然后A、B线程开始抢,看谁能把各自LR指针给贴在坑位。也就说如果有任意一个线程抢到了这把轻量级锁的时候,这把轻量级锁里面就记录了指向线程栈中LockRecord的指针,占62位。这个抢的过程是采用自旋的方式,即CAS。用CAS操作把里面62位修改为指向我自己线程栈的LR指针。
竞争加剧后(JDK1.6之前指有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程数超过CPU核数的一半。JDK1.6之后,加入自适应自旋 Adapative Self Spinning,JVM自己控制。)升级为重量级锁。重量级锁存在于内核态,本质就是mutex数据结构,有数量限制的。用户态想申请重量级锁的时候,得像内核申请。内核给了我这把锁,我才拥有这把锁,这个时候在markword中占62位来记录指向重量级锁的指针。
那么为什么要从轻量级锁升级到重量级锁呢?轻量级锁本质就是执行一个循环,运行在用户态,效率高。但是这个循环是需要不断消耗CPU,如果我们竞争特别激烈,一个线程始终占着,10000个线程都在自旋,那CPU就很快100%了。升级为重量级锁后,维护有一个队列,而在这个队列里,如果没有轮到我执行的时候,是不消耗CPU的,处于wait状态。
下面多提2个概念:
锁消除 Lock eliminate
public void add(String str1, String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道StringBuffer是线程安全的,因为它的关键方法是被synchronized修饰过的,但我们看上面这段代码,我们就会发现,sb这个引用只会在add方法中使用,不可能被其他线程引用(因为是局部变量,栈私有),因此sb是不可能共享的资源,JVM会自动消除StringBuffer对象内部的锁。因为你不断的append(str1).append(str2).append(str3)......有锁或无锁是没有区别。
锁粗化 Lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer();
while(i<100){
sb.append(str);
i++;
}
return sb.toString();
}
JVM会检测到这样一连串的操作都对同一个对象加锁(while循环内100次执行append,没有锁粗化就要进行100次加锁/解锁),此时JVM就会把加锁的范围放在while外部,使得一连串操作只需要加一次锁即可。
二.synchonized的实现过程
1.在java代码层面,就是加了一个synchronized关键字
2.在字节码层面,就是加了monitorenter指令和monitorexit指令。进入锁状态和退出锁状态。
3.在JVM执行过程中,自动进行上述锁升级。
4.在汇编层面,就是用了lock comxchg这条指令。
IF_MP 指如果是多个CPU。
至此便是锁的底层实现。