synchronized锁升级_浅析synchronized底层实现与锁升级过程

在Java中,synchronized关键字是用来控制线程同步的。就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

那么synchronized具体是怎么做到线程同步的呢?还有锁升级过程的过程是怎样的的?我们来探讨一下。

0x01 synchronized实现细节

1.1 Java代码实现

我们先来了看下如果多线程间竞争共享资源,不采取措施会出现什么情况:

public class TestSync implements Runnable {    private int count = 100;    public static void main(String[] args) {        TestSync ts = new TestSync();        Thread t1 = new Thread(ts, "线程1");        Thread t2 = new Thread(ts, "线程2");        Thread t3 = new Thread(ts, "线程3");        t1.start();        t2.start();        t3.start();    }    @Override    public void run() {        while (true) {            if (count > 0) {                count--;                System.out.println(Thread.currentThread().getName() + " count = " + count);            } else {                break;            }        }    }}
9413a83d5dee8c663bcf43fc359b10e9.png

线程2将count减到了97,线程3、线程1在某一刻也做了count--,但是结果却也是97,说明他们在做count--的时候并不知道有别的线程也操作了count。

这个问题,相信大家都知道加synchronized可以解决。

对run方法作如下修改:

@Overridepublic void run() {    while (true) {        synchronized (this) {            if (count > 0) {                count--;                System.out.println(Thread.currentThread().getName() + " count = " + count);            } else {                break;            }        }    }}
85bfdd6c4b551a33b74307078db45505.png

执行count--有条不紊,不会出现不安全的问题。

因此,在代码层面,加关键字synchronized能解决上述线程安全问题。

1.2 字节码层面如何实现synchronized

如果使用IDEA的话,这里推荐安装一个jclasslib Bytecode viewer,这个插件可以很方便的看程序字节码执行指令:

359a79c17339a071aab44387b0388a5b.png

我们来看一下刚才的程序字节码指令:

df5f0bd6d38e0154be1169f1feb1b028.png
f2877f29508a780d4d27b318a82f13fb.png
68e4fb00eba5857f03e116394c27efd4.png

实际上synchronized的实现从字节码层面来看,就是monitorenter和monitorexit指令,这两个就可以实现synchronized了。

「monitorenter」

Java对象天生就是一个Monitor,当monitor被占用,它就处于锁定的状态。

每个对象都与一个监视器关联。且只有在有线程持有的情况下,监视器才被锁定。

执行monitorenter的线程尝试获得monitor的所有权:

  • 如果与objectref关联的监视器的条目计数为0,则线程进入监视器,并将其条目计数设置为1。然后,该线程是monitor的所有者。
  • 如果线程已经拥有与objectref关联的监视器,则它将重新进入监视器,从而增加其条目计数。这个就是锁重入。
  • 如果另一个线程已经拥有与objectref关联的监视器,则该线程将阻塞,直到该监视器的条目计数为零为止,然后再次尝试获取所有权。

「monitorexit」

一个或多个MonitorExit指令可与Monitorenter指令一起使用,它们共同实现同步语句。

尽管可以将monitorenter和monitorexit指令用于提供等效的锁定语义,但它们并未用于同步方法的实现中。

JVM在完成monitorexit时的处理方式分为正常退出和出现异常时退出:

  • 常规同步方法完成时监视器退出由Java虚拟机的返回指令处理。也就是说程序正常执行完毕的时候,JVM有一个指令会隐式的完成monitor的退出---monitorexit,这个指令是athrow。
  • 如果同步语句出现了异常时,JVM的异常处理机制也能monitorexit。
bffe365beef236f78492619a194fb532.png

简单的加锁解锁过程

因此,执行同步代码块后首先要执行monitorenter指令,退出的时候monitorexit指令。

1.3 JVM层实现

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

执行结果:

java.lang.Object object internals: OFFSET  SIZE   TYPE DESCRIPTION                               VALUE      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)     12     4        (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totaljava.lang.Object object internals: OFFSET  SIZE   TYPE DESCRIPTION                               VALUE      0     4        (object header)                           08 f3 7f 02 (00001000 11110011 01111111 00000010) (41939720)      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)     12     4        (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

没有加synchronized的时候,对象头信息的值为01 00 00 00,加了锁之后,对象头变了08 f3 7f 02,说明synchronized会修改对象的头新信息,对象头在Hotspot里面叫做markword。

一个对象的markword里面有非常重要的信息,其中最重要的就是锁synchronized。(markword里还有GC的信息,还有hashcode的信息。)

「Hotspot实现的JVM在64位机的markword信息」

0f5cad4eec29ef186798ce13f1e75677.png

markword信息

0x02 锁升级过程

2.1 升级过程

在JDK早期的时候,synchronized的底层实现是重量级的,所谓重量级,就是它直接去找操作系统去申请锁,它的效率是很低的。

JDK后来对synchronized锁进行了优化,这样才有了锁升级的概念。

锁升级的过程大致是这样的:

new -> 「偏向锁」 -> 「轻量级锁 (自旋锁)」-> 「重量级锁」

synchronized优化的过程和markword息息相关。

用markword中最低的三位代表锁状态,其中1位是偏向锁位,最后两位是普通锁位。

  1. Object o = new Object()

锁 = 0 01 无锁态

注意:如果偏向锁打开,默认是匿名偏向状态

  1. o.hashCode()

001 + hashcode

  1. 默认synchronized(o)

00 -> 轻量级锁

默认情况,偏向锁有个时延,默认是4秒

why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

可以用BiasedLockingStartupDelay参数设置是否启动偏向锁(=0,立即启动偏向锁):

-XX:BiasedLockingStartupDelay=0
  1. 如果启动了偏向锁

锁升级过程:new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock

打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101

  1. 如果有线程上锁

上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程。

偏向锁不可重偏向、批量偏向、批量撤销

  1. 如果有线程竞争

撤销偏向锁,升级为轻量级锁

线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁

  1. 如果竞争加剧

竞争加剧:有线程超过10次自旋, (-XX:PreBlockSpin参数可调),或者自旋线程数超过CPU核数的一半, JDK 1.6之后,加入自适应自旋 Adapative Self Spinning ,JVM自己控制。

升级重量级锁:向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。

总结一下,锁升级的过程大概是这样的:

459fccf66d65e3cd1c135346df0f1eac.png

锁升级过程

2.2 为什么有自旋锁了还需要重量级锁

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

2.3 偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。

JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。

2.4 synchronized最底层实现

在硬件层面,锁其实是执行了lock cmpxchg xx指令。

synchronized在字节码层面:

如果锁的是方法,jvm会加一个synchronized修饰符;

如果是同步代码快,就是用monitorenter和monitorexit指令。

当jvm看到了synchronized修饰符或者monitorenter和monitorexit的时候,对应的就是C++调用操作系统提供的同步机制。

CPU级别是使用lock指令来实现的。

比如,我们要在synchronized某一块内存上设置一个数i,把i的值从0变成1,这个过程放在CPU执行可能会有好几条指令或者不能同步(速度太快),所以需要有个lock指令。

cmpxchg前面如果加了一个lock的话,后面的指令执行过程中对这块区域进行锁定,只有这条指令可以修改,其他指令是不能操作的。

0x03 小结

  • Java对象头 「markword」 在Hotspot虚拟机中,对象在内存中的布局分为三块区域: 对象头实例数据对齐填充

Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

  • 「monitor」

一个同步工具,也可以描述为一种同步机制。

为什么每个对象都可以成为锁呢? 因为每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,而对应的 oop/oopDesc 都会存在一个markOop 对象头,而这个对象头是存储锁的位置,里面还有对象监视器,即ObjectMonitor,所以这也是为什么每个对象都能成为锁的原因之一。

  • 「锁升级(优化)过程」

synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁。

  • 「偏向锁」

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的markword里是否存储着指向当前线程的偏向锁。

开启:「-XX:BiasedLockingStartupDelay=0」

  • 「自旋锁」

自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。

JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制。

  • 「重量级锁」

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

作者:行百里er
链接:https://juejin.im/post/6888112467747176456
来源:掘金

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值