JAVA并发编程——Synchronized与锁升级

1.Synchronized的性能变化

2.synchronized锁种类及升级步骤

3.JIT编译器对锁的优化

4.总结

1.Synchronized的性能变化
我们都知道synchronized关键字能够让程序串行化执行,保证数据的安全性,但是性能会下降
所以java对synchronized进行了一系列的优化
java5之前
synchronized仅仅只是synchronized,这个操作是重量级别的操作,cpu在进入加锁的程序后,会进行用户态和内核态之间的切换

JAVA并发编程——Synchronized与锁升级_第1张图片
用户态:用户态运行用户程序,级别较
内核态:内核态运行操作系统程序,操作硬件,级别较

java如果要阻塞或者唤醒一个线程需要操作系统的接入,需要在用户态和核心态之间切换,因为synchronized属于重量级锁,是需要依赖底层操作系统的Mutex Lock来实现的挂起线程和恢复线程都需要进入内核态去完成,这种切换会消耗大量的系统资源,如果同步代码块中的内容过于简单,这种切换的时间可能比用户代码的执行时间还长,时间成本太高,这也是为什么早起synchronized效率低的原因。

java6开始:
优化Synchronized,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁,轻量级锁和重量级锁(减少了线程的阻塞和唤醒)

2.synchronized锁种类及升级步骤
在说synchronized锁升级之前,我们要先搞清楚,线程访问一个synchronized修饰的方法,有三种类型:
1)只有一个线程来访问,有且唯一。
2)有两个线程A,B来交替访问
3)竞争激烈,多个线程来访问

还记得我们上一篇博客 JAVA并发编程——Java对象内存布局和对象头中提到了对象头,我们先来看看这张图:
JAVA并发编程——Synchronized与锁升级_第2张图片
我们可以看出synchronized用的锁是存在java对象头的Mark Word中,锁升级功能主要依赖Mark Word中锁标志位和释放偏向锁标志位。

java的锁升级按照
无锁->偏向锁->轻量级锁->重量级锁
我们挨个进行讲解。

无锁:
我们先看一段无锁的代码

    public static void main(String[] args) {

        //-XX:-UseCompressedClassPointers -XX:BiasedLockingStartupDelay=0
        Object o = new Object();

        System.out.println(ClassLayout.parseInstance(o).toPrintable());

    }

这个对象没有使用锁,我们看一下运行的结果。
JAVA并发编程——Synchronized与锁升级_第3张图片
这个输出的结果,对象头是倒着输出的,标红的地方便是锁标志位,现在是001,对应的对象头的图就是无锁状态。

偏向锁:
当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问的时候,就会自动获得锁。(这个锁偏向了经常访问的线程。)

在测试偏向锁之前,记得先输入jvm参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 因为偏向锁默认在jdk1.6之后默认是开启的,但是启动时间有延迟,所以需要手动添加参数,让偏向锁的延时时间为0,在程序启动时立刻开启。

        Object o = new Object();

        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t1").start();

运行结果为:
JAVA并发编程——Synchronized与锁升级_第4张图片

轻量级锁:
刚刚上面讲述的只是有一个线程正在争抢一个资源类,但是现在有另外线程来逐步竞争锁的时候,就不能使用偏向锁了,要升级为轻量级锁
在第一个线程正在执行synchronized方法(处于同步块),当它还没有执行完的时候,其它线程来抢夺,竞争线程使用cas更新对象头失败,该偏向锁会被取消并出现锁升级
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获取该轻量级锁。

//关闭延时参数,启用该功能
        Object o = new Object();

        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t1").start();

JAVA并发编程——Synchronized与锁升级_第5张图片

那么竞争到底自旋多少次会进行锁升级呢?

java6之前:
默认情况下自旋的次数为10次:-XX:PreBlockSpin=10
或者自旋次数超过cpu核数一半

java6之后:
自适应:意味着自旋次数是不固定的。
是根据:同一个锁上次自旋的时间和拥有锁线程的状态来确定

偏向锁和自旋锁的区别:
争夺轻量级锁失败的时候,自旋尝试抢占锁
轻量级锁每次退出同步代码块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

重锁:
有大量线程正在抢占同一个资源类,冲突性很高,会升级成重量级锁。

        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t2").start();
        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t3").start();

JAVA并发编程——Synchronized与锁升级_第6张图片

3.JIT编译器对锁的优化

1)锁消除
当只有一个线程运行synchronized代码的时候,默认会把锁消除,节省资源。

/**
 * 锁消除
* 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
* 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
*/
public class LockClearUPDemo
{
static Object objectLock = new Object();//正常的

public void m1()
    {
//锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
Object o = new Object();

synchronized (o)
        {
            System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
        }
    }

public static void main(String[] args)
    {
        LockClearUPDemo demo = new LockClearUPDemo();

for (int i = 1; i 10; i++) {
new Thread(() -> {
demo.m1();
            },String.valueOf(i)).start();
        }
    }
}

2)锁粗化
加入一个锁在同一个方法中,头尾相接,前后相邻的都是一个锁对象,那么编译器就会把这几个synchronized合并成一大块,加粗了范围,节省了资源。

/**
 * 锁粗化
* 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
* 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
*/
public class LockBigDemo
{
static Object objectLock = new Object();


public static void main(String[] args)
    {
new Thread(() -> {
synchronized (objectLock) {
                System.out.println("11111");
            }
synchronized (objectLock) {
                System.out.println("22222");
            }
synchronized (objectLock) {
                System.out.println("33333");
            }
        },"a").start();

new Thread(() -> {
synchronized (objectLock) {
                System.out.println("44444");
            }
synchronized (objectLock) {
                System.out.println("55555");
            }
synchronized (objectLock) {
                System.out.println("66666");
            }
        },"b").start();

    }
}

4.总结

锁升级的流程图片
JAVA并发编程——Synchronized与锁升级_第7张图片

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法只有纳秒的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高了线程的相应速度 始终得不到cpu的话,会空转,浪费cpu 追求相应时间,同步块执行速度非常快
重量级锁 线程竞争不适用自旋,不消耗cpu 线程阻塞,造成用户态和内核态的切换,响应时间慢 追求数据一致性,同步执行块执行速度较长

锁升级用一句话概括:
先自旋,不行再阻塞。
就是把之前的悲观锁(重量级锁)在变成一定条件下使用偏向锁以及使用轻量级锁。

synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

你可能感兴趣的:(JAVA并发编程——Synchronized与锁升级)