synchronized锁升级过程

JAVA虚拟机对synchronized的优化

对象头与monitor

MarkWord区

是存在在JAVA对象头中的一个区域大小为8字节
里面包含了
1.锁信息
2.GC信息
3.HashCode(如果有调用)

public class Demo1 {
    public static void main(String[] args) throws IOException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

输出

synchronized锁升级过程_第1张图片
加锁后,对象头上的MarkWord被改变

1.无锁

在MarkWord中锁标志位为 001
在这里插入图片描述
通过代码验证

public class Demo {
    public static void main(String[] args) throws IOException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());     
    }
}

这段代码通过JOL工具打印出来是 最后2位是00,正好对应上面的无锁
在这里插入图片描述

2.偏向锁(用户空间完成)

在MarkWord中锁标志位为 101
在这里插入图片描述

2.1为什么有会偏向锁?

public class Demo1 {
   private static int i = 1;
    public void inc(){
        synchronized (Demo1.class) {
            i++;
        }
    }
}

上面这段代码,假设你并发量不高,可能inc()并不会并发执行,但是以防万一,所以你为了保证i++没有并发安全问题于是你加了synchronized 关键字保证安全,如果没有竞争情况下使用可以就是一个偏向锁,因为没有竞争所以没有必要使用轻量锁、重量锁。

2.2偏向锁原理

当没有并发竞争的情况下:
因为你的系统并发并不高,synchronized 的互斥机制一次都没有被触发过,也就是没有同一个时刻去执行i++,所以synchronized 认为你现在并没有出现竞争的情况,一直是单线程执行,它把当前执行这段代码的线程的指针放到对象头中,表示有一个线程在干活,并且设置为偏向锁,偏向这个线程。当这个线程执行完成后,另外一个线程来了,再把当前线程指针去覆盖上次的线程指针,以上是在没有并发竞争的情况下的逻辑。

当存在并发竞争的情况下:
此时synchronized 有多线程进行竞争了,这是多个线程都把想自己的线程指针放到对象头中让自己区执行,这个时候偏向锁会触发“ 撤销偏向锁”,还原对象MackWord区域为无锁状态,用到CAS去决定,谁可以把自己的线程指针到对象头中,谁就是抢到这把锁了,这个时候升级成一个轻量级锁 (又称:自旋锁/CAS/无锁)

2.3偏向锁的延迟启动

在创建一个对象后,这个对象头的标志位为 001,是个无锁状态,但是等待4秒之后在创建一个对象,则是对象头是一个偏向锁状态,标志位为 101。
为什么怎么做?
JVM启动的时候一定是用到了多线程,所以明确的知道资源会出现竞争的情况,必然出现锁撤销,升级到轻量锁,所以前4秒直接禁用偏向锁。
那前4秒不禁用偏向锁呢?
使用偏向锁竞争激烈然后触发 “ 撤销偏向锁” ,还原对象MackWord区域为无锁状态,锁升级到轻量级锁。经历这一套流程最后还是会到轻量锁,“ 撤销偏向锁” 和还原对象MackWord区域也是一个消耗资源的地方,所以前4秒禁用偏向锁,使用轻量级锁。

可以通过JVM参数 -XX:BiasedLockingStartupDelau 设置延迟多少秒启动偏向锁,默认是4

public class Demo1 {
    public static void main(String[] args) throws IOException, InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        Thread.sleep(5000);
        Object o1 = new Object();
        System.out.println(ClassLayout.parseInstance(o1).toPrintable());
    }
}

输出
synchronized锁升级过程_第2张图片

3.轻量级锁(用户空间完成)

在MarkWord中锁标志位为 00
在这里插入图片描述

public class Demo {
	public static void main(String[] args) throws IOException {
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
 		}
    }
}

这段代码通过JOL工具打印出来是 最后2位是00,正好对应上面的轻量级锁
在这里插入图片描述

3.1轻量级锁原理

此时的轻量级锁由一个偏向锁升级而来,当前有一个线程持有了锁,其他线程就去进行自旋做CAS操作,其他线程就一直尝试使用CAS把自己的线程指针给放到对象头中,当持有锁的线程执行完后,其他在进行CAS操作的线程,某一个线程使用CAS成功把自己线程指针给放到对象头中。
但是,如果抢到锁的那个线程执行的时候很长,一直不释放锁,其他线程一直在自旋做CAS操作,一直拿不到锁,疯狂占用CPU资源。所以一直等下去不是办法,这时就升级到了重量级锁
升级条件
1.6自旋超过10次或超出CPU核数的2/1,并且有参数可以调整
1.7之后使用的是自适应自选,由JVM根据历史数据自己去计算应该自旋多少次升级

4.重量级锁(内核申请)

在MarkWord中锁标志位为 10
在这里插入图片描述
synchronized锁升级过程_第3张图片

4.1.重量级锁原理

因为竞争太过于激烈,总不能一直让CAS做无用的自选,所以为了减少系统资源浪费,升级到了重量级锁,把等待的线程放到等待队列(把线程冻结了),不消耗CPU资源

5.锁升级过程总结

  • 偏向锁升级到轻量锁:只要出现了竞争锁就升级到轻量锁
  • 偏向锁升级到重量锁:线程中调用wait方法,当前线程阻塞,被丢到线程等待队列中,升级为总量锁
  • 轻量锁升级到重量锁:轻量锁自旋超过一定次数升到到重量锁,这个次数由JVM自适应自旋计算出来的
  • synchronized锁升级过程_第4张图片

6.常见问题

  1. 偏向锁的效率一定比自旋锁效率高吗?并不是
    当你明确的知道你的加锁资源会出现竞争的情况,那么给它设置偏向锁是多余了,直接就应该是设置为一个轻量锁(自旋锁),

7.锁状态图

synchronized锁升级过程_第5张图片

你可能感兴趣的:(JVM,java)