重量级锁
Java的对象头中的MarkWord
32位操作系统:
64位操作系统:
Monitor
- 当使用
synchronized
获得锁时:
synchronized(obj){//重量级锁
//临界区代码
}
obj
对象的MarkWord中的指针(ptr_to_heavyweight_monitor)指向一个 OS提供的 Monitor对象
Monitor中的Owner记录谁是这个锁的主人。
- 当另一个对象也要获取
obj
锁时:
发现obj
所指向的Monitor
的所有者为Thread1,此时Thread2加入 阻塞队列
- 当Thread1执行完毕,释放锁后,虚拟机从
obj
对象指指向的Monitor的EntryList
中唤醒一个线程,赋给它锁。
轻量级锁
使用场景:如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(没有竞争),那么可以使用 轻量级锁 来优化。
轻量级锁的语法仍然是synchronized
static final Object obj = new Object();
public static void method1(){
synchronized(obj){ //1 加锁
method2();
}//4
}
public static void method2(){
synchronized(obj){ //2
...
}//3
}
加锁:
- 在栈帧中创建锁记录(Lock Record)对象(代码1):
左边是栈帧,右边是Java堆中的obj
对象,每个线程的栈帧都会包含一个锁记录的结构,其中存储了锁定对象的Mark Word。
- 让锁记录中Object reference指向锁对象,并尝试使用cas替换Object的Mark Word,将Mark Word的值存入锁记录。
- 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁
- 如果cas失败,有两种情况:
-
如果有其它线程持有了
obj
的轻量级锁,这是表明有竞争,进入锁膨胀过程 -
如果是自己执行了
synchronized
锁重入 (代码中2的位置),那么再添加一条LockRecord作为重入的计数
这里也会进行cas替换操作,但是会失败。
解锁:
- 在代码3的位置,退出
synchronized
代码块,如果有取值为null的所记录,表示有重入,这是重置锁记录,表示重入计数减一
- 在代码4的位置,退出
synchronized
代码块,此时所记录的值不为null,这是使用cas将Mark Word的值恢复给obj
对象头
-
成功,解锁成功
-
失败,说明轻量级锁进行了膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
static final Object obj = new Object();
public static void method1(){
synchronized(obj){ //1 加锁
method2();
}//4
}
Thread-0执行method1
方法,获得到轻量级锁,此时Thread-1执行method1
方法,获取轻量级锁失败,进入锁膨胀流程:
-
为
ojb
对象申请 Monitor 锁,让obj
指向重量级锁地址,并且对象头所标志位改为10
-
然后自己进入Monitor的EntryList 进入阻塞队列
- 当Thread-0 退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入 重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
自旋优化
当轻量级锁竞争时,会先进行自旋等待锁,如果自旋没有获得锁,才会膨胀为重量级锁
偏向锁
轻量级锁在每次锁重入时,仍然需要执行CAS操作,JAVA 6 中引入了偏向锁,来优化锁轻量级锁的锁重入。
static final Object obj = new Object();//1
public static void method1(){
synchronized(obj){ //2
method2();
}//5
}
public static vpid method2(){
synchronized(obj){ //3
//临界区
}//4
}
-
1: 此时
obj
的对象头的Mark Word为:
-
2:假设Thread-1是第一个获取该锁的线程,其线程ID为100,那么此时的Mark Word为:
-
3:这里是一个锁重入,此时
obj
的对象头的Mark Word与2一样 -
4:在重入锁释放锁的时候,偏向锁使用了一种 等到竞争出现才释放锁 的机制,这里也不会改变Mark Word ,同理在代码5处释放锁的时候也不会改变对象头
public static void test1(){
Object lock = new Object();
Thread t1 = new Thread(()->{
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
},"t1");
t1.start();
Thread t2 = new Thread(()->{
try {
t1.join();//等待t1线程终止
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
},"t2");
t2.start();
}
运行结果:
从上面的运行结果可以看到:t2线程在获取锁的时候,因为对象头的线程id不是t2线程的id,所以偏向锁 升级为了轻量级锁;并且在释放锁之后,lock对象的Mark Word 被设置为无锁状态(001),那么下次线程获取改锁时会是一个 轻量级锁。
偏向锁撤销
- hashcode
Object lock = new Object();
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
d.hashCode();
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
对象的哈希码是存放在Mark Word 中的,但是一旦存放了哈希码,Mark Word就没有多余的空间来存放线程Id了,此时lock
对象的Mark Word 的锁状态就变为了无锁(001)。
- 其他线程竞争锁
如前面demo的结果所示,t2线程在t1线程终止之后获得了一个轻量级锁。t2在申请锁的过程中,偏向锁被撤销,然后升级为轻量级锁赋给t2线程,t2线程释放锁后,lock
对象的锁状态为无锁状态。
偏向锁的批量重偏向
在上面的偏向锁撤销中,我们发现当一个线程去竞争偏向其他线程的偏向锁时,偏向锁会撤销。偏向锁的撤销有这样一个机制:
如果有线程t竞争偏向其他的线程的次数累计达到19次,那么第19次之后竞争的偏向锁将会偏向线程t(线程t获得的不再是轻量级锁),这个机制叫做 批量重偏向。
public static void test2() throws InterruptedException {
List<Object> locks = new ArrayList<>();
Thread t1 = new Thread(()->{
for (int i = 0; i < 30 ; i++){
Object lock = new Object();
locks.add(lock);
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
try {
t1.join();//等待t1结束
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 30; i++){
Object lock = locks.get(i);
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
},"t2");
t2.start();
}
运行结果:
可以看到第19个锁还是轻量级锁,第20个锁已经偏向了t2。
偏向锁的批量撤销
public static void test3() throws InterruptedException {
List<Object> locks = new ArrayList<>();
Thread t1 = new Thread(()->{
for (int i = 0; i < 39 ; i++){
Object lock = new Object();
locks.add(lock);
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 39; i++){
Object lock;
lock = locks.get(i);
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
},"t2");
t2.start();
Thread t3 = new Thread(()->{
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 39; i++){
Object lock;
lock = locks.get(i);
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
synchronized (lock){
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
}
},"t3");
t3.start();
t3.join();
log.info("新建Lock对象:" + getMarkWord(ClassLayout.parseInstance(new Object()).toPrintable()));
log.info("新建Object对象:" + getMarkWord(ClassLayout.parseInstance(new Object()).toPrintable()));
}
t2线程撤销了0-18个锁,19-38个锁进行了重偏向线程t2。
t3线程撤销了19-38个锁,在JVM中,Lock
类型的锁一共被撤销了39次,我们看新建一个Lock
对象,它的所类型如下:
从结果可以看到,新创建的Lock
对象不再试偏向锁了。这就是偏向锁的批量撤销机制:
在JVM中,属于同一类型的偏向锁被撤销大于等于39次,以后新建该类型对应的锁将不再是偏向锁。
锁消除
public class LockRemoveTest{
static int i = 0;
public void method(){
Object obj = new Object();
synchronized (o){
i++;
}
}
}
如果上面的代码被反复的执行超过一个阈值,JIT即时编译器就会优化这部分的字节码。其中,类似obj
这样的 局部变量,它除了在本方法中可以被访问到,其他任何地方都无法访问,所以这里的同步块就没有任何意义,JIT就会优化这个同步块,取消同步操作。
我们可以使用-XX:-EliminateLocks
来关闭锁消除优化。