1.synchronized
我们都知道synchronized内部有四种状态,分别是:无锁、偏向锁、轻量级锁和重量级锁,所以要搞懂这几种锁之间的变化我们得对synchronized有个大致的了解。
首先说一下synchronized在底层的实现,他是基于进入和退出Monitor对象(每一个对象都会有一个monitor与之关联)来实现方法同步和代码块同步的,对于方法同步官方并没有具体指出是如何实现的,而对于同步代码块是通过monitorenter和monitorexit指令来实现的。在进入同步代码的地方执行monitorenter指令,在代码执行结束和异常处执行monitorexit指令,这样做是为了不至于在出现异常的时候无法释放锁,避免死锁。空口无凭,我们来看一下代码。
public class MonitorTest {
Object lock = new Object();
public int test(int i){
synchronized (lock){
return i-1;
}
}
}
经过反编译之后可以得到
public int test(int);
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_2
6: monitorenter
7: iload_1
8: iconst_1
9: isub
10: aload_2
11: monitorexit
12: ireturn
13: astore_3
14: aload_2
15: monitorexit
16: aload_3
17: athrow
}
从上面我们可以看出有一个monitorenter但是却有两个monitorexit,就是为了防止异常时无法释放锁。
2.对象头
synchronized的锁时存放在对象头里的,所以我们得对对象头有一定的了解,这里我们不细讲,可以参考深入理解JVM学习笔记。
我们 synchronized锁的信息是存在对象头里一个叫Mark Word的区域里的 ,在运行期间,Mark Word里存储的数据会随锁状态的变化而发生变化,我们来看一下
下面开始我们今天的正式内容,锁膨胀
3.锁膨胀
首先我们得知道锁时只能升级不能降级的(这里说的是有线程占有锁的情况,如果没有任何一个线程占有锁,锁是可以降级成无锁的),也就是偏向锁升级成轻量级锁就不能再降级后才能偏向锁了,然后我们看一下锁膨胀的流程图。
我们从头开始,一步步讲解锁膨胀的过程。
偏向锁和无锁
我们先介绍一下偏向锁和无锁,偏向锁在Java 6和Java 7是默认启动的,但是在程序启动几秒之后才可以激活,这个值默认是4秒,在这之前创建的对象就是无锁状态,可以通过-XX:BiasedLockingStartupDelay=0来关闭延迟,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false。
如果JVM启用偏向锁,那么一个新建未被任何线程获取的对象Mark Word中的Thread Id为0,是可以偏向但未偏向任何线程,被称为匿名偏向状态。而无锁状态是不可偏向也未偏向任何线程,不可再变为偏向锁。记住!无锁状态不能变成偏向锁!
偏向锁有三种状态:
- 匿名偏向:这是允许偏向锁的初始状态,其Mark Word中的Thread ID为0,第一个试图获取该对象锁的线程会遇到这种状态,可以通过CAS操作修改Thread ID来获取这个对象的锁。
- 可重偏向:这个状态下Epoch是无效的,下一个线程会遇到这种情况,在批量重偏向操作中,所有未被线程持有的对象都会被设置成这个状态。然后在下个线程获取的时候能够重偏向。
- 已偏向:这个状态最简单,就是被线程持有着,此时Thread ID为其偏向的线程。
可能有人不理解批量重偏向是什么意思,这里我主要讲解一下,顺便说一下批量撤销。
首先说明一下,批量重偏向和批量撤销都是针对类的优化,与不是针对单个对象。
批量重偏向:当一个线程创建了大量对象(同一个class的)并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。 具体是多少个对象呢?Hotspot默认是二十个( 当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold 来手动设置阈值 ),意思就是如果线程A创建了二十个以上的对象(相同class的)并获取该对象的偏向锁,在其释放锁后又有一个线程获取这些对象,那么一旦线程B获取对象锁的数量达到二十个,这之后的对象就可以重偏向(之前的是轻量级锁),依旧可以作为偏向锁使用,但是二十个之前的则会升级为轻量级锁。但是一定要注意:**重偏向只能触发一次!**如果又有第三个线程C获取已经重偏向过的锁会直接升级成轻量级锁。
批量撤销:批量撤销就是如果线程B在达到批量重偏向之后继续获取对象锁,这个数量达到了批量撤销阈值(默认是40,可以通过 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值),那么JVM就认为当前场景存在多线程竞争,会标记该class不可偏向,之后再对于该class对象的锁直接走轻量级锁流程(即使是新建对象)。注意:触发批量撤销的线程仍然能够使用偏向锁(即使是新建对象),是从下一个线程开始变成轻量级锁.
批量撤销:批量撤销就是如果线程B在达到批量重偏向之后继续获取对象锁,这个数量达到了批量撤销阈值(默认是40,可以通过 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值),那么JVM就认为当前场景存在多线程竞争,会标记该class不可偏向,之后再对于该class对象的锁直接走轻量级锁流程(即使是新建对象)。注意:触发批量撤销的线程仍然能够使用偏向锁(即使是新建对象),是从下一个线程开始变成轻量级锁。
参考自偏向锁批量重偏向与批量撤销
重量级锁
重量级锁是通过互斥量(Mutex)来实现的 ,一个线程获取到锁进入同步块,在没有释放锁之前,会阻塞其他未获取锁的线程。
轻量级锁
1. 线程在执行同步块之前,会在栈帧里创建一个存储锁记录(Lock Record)的空间,并把对象头里的Mark Word复制到锁记录里(官方成为Displaced Mark Word),然后JVM会使用CAS操作将对象头里的Mark Word更改为指向锁空间的指针。
2. 如果更新成功了就获取到这个对象的轻量级锁
3. 如果更新失败了首先会检查对象的Mark Word是否指向当前的线程,如果指向当前的线程,说明4. 该线程已经获取这个这个对象的锁了,继续执行同步块代码。
5. 如果不指向当前线程,表示有其他线程竞争锁,当前线程便尝试自旋获取锁。如果在这过程中获取到了,那就执行同步块代码。
6. 如果自旋一定次数还没竞争到锁,就将锁升级为重量级锁,当前线程阻塞。
如果持有线程释放锁失败(CAS替换Mark Word,因为有其他线程在争夺锁),那么将释放锁并唤醒等待的线程
偏向锁——>轻量级锁
1. 线程第一次访问同步代码块,首先他会检查这个对象的Mark Word中的锁标志位是多少,如果是01,则进入第2步,依此来判断是否是无锁状态或者是偏向锁状态,如果00(轻量级锁),则进入第1步,如果是10(重量级锁),则进入第2步。
2. 继续判断是否是偏向锁,如果是偏向锁则进入第3步,如果不是则进入第 步。
3. 接着判断对象Mark Word中的Tread ID是否是当前线程ID,如果是就代表当前线程已经获取到这个对象锁,以后再获取对象锁的时候不需要再进行CAS操作, 只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来记录重入的次数。释放锁时依次删除锁空间里的Lock Record,当记录释放完之后就代表着释放了这个对象的锁。 这里有一点需要注意:偏向锁的释放并不是主动,而是被动的,持有偏向锁的线程执行完同步代码后不会主动释放偏向锁,而是等待其他线程来竞争才会释放锁 ,就是Thread ID依旧不会改变。
4. 如果当前对象Mark Word中的Thread ID不是当前线程,那么就会使用CAS操作修改Thread ID,如果这个对象是匿名偏向状态就可以修改成功,成功获取锁,如果不是就说明对象锁已被其他线程占有,则修改失败,执行第5步。
5. 修改失败后就会进行锁撤销,锁的撤销需要等待一个全局安全点(当前没有执行的字节码),然后暂停拥有这个偏向锁的线程,判断这个线程是否依然存活。如果是执行第6步,否则第7步
6. 如果持有线程依旧存活,判断是否还在执行同步代码块(根据Lock Record),如果还在执行,则升级为轻量级锁,持有线程获取轻量级锁(JVM将对象Mark Word与Lock Record交换),获取线6程CAS竞争获取锁。
7. 如果对象已死亡或不在执行同步代码块,则判断该对象是否可重偏向。如果可以则将对象设置成匿名偏向状态,则将使用CAS操作将偏向锁重新指向当前线程,获取到偏向锁,执行同步代码块。如果不可重偏向,就将对象设置为无锁状态,然后升级为轻量级锁,CAS获取锁。
8. 恢复暂停的线程,继续执行代码
注:持有线程是持有偏向锁的线程,当前线程是想要获取偏向锁的线程