说一下synchronized底层实现原理?_synchronized底层实现原理及锁优化

作者:0oh28h327 链接:jianshu.com/p/c8f997e7f75c 一、简述 synchronized的作用
①原子性:synchronized 保证语句块内操作是原子的。
②可见性:synchronized 保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)。
③有序性:synchronized 保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)。 synchronized的使用
①修饰实例方法,对当前实例对象加锁。
②修饰静态方法,多当前类的Class对象加锁。
③修饰代码块,对 synchronized 括号内的对象加锁。 二、实现原理 JVM 的同步(synchronized)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。 方法级的同步
在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor (虚拟机规范中用的是管程一词),然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor。 代码块的同步
代码块的同步是利用 monitorenter 和 monitorexit 这两个字节码指令。synchronized 是 JVM 实现的一种互斥同步访问方式,底层是基于每个对象的监视器(monitor)来实现的。被 synchronized 修饰的代码,被编译器编译后在前后加上了一组字节指令。
①在代码开始加入了 monitorenter,在代码后面加入了 monitorexit,这两个字节码指令配合完成了 synchronized 关键字修饰代码的互斥访问。
②在虚拟机执行到 monitorenter 指令的时候,会请求获取对象的 monitor 锁,基于 monitor 锁又衍生出一个锁计数器的概念。
③当执行 monitorenter 时,若对象未被锁定时,或者当前线程已经拥有了此对象的 monitor 锁,则锁计数器+1,该线程获取该对象锁。
④当执行 monitorexit 时,锁计数器-1,当计数器为0时,此对象锁就被释放了。那么其它阻塞的线程则可以请求获取该 monitor 锁。
⑤如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。 这里要注意:
①synchronized 是可重入的,所以不会自己把自己锁死
②synchronized 锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。 关于ACC_SYNCHRONIZED 、monitorenter、monitorexit指令,可以看一下下面的反编译代码:
public class SynchronizedDemo {
    public synchronized void f(){//这个是同步方法
        System.out.println("Hello world");
    }
    public void g(){
        synchronized (this){//这个是同步代码块
            System.out.println("Hello world");
        }
    }
    public static void main(String[] args) {
    }
}
使用javap -verbose SynchronizedDemo反编译后得到:

477eb9ac55b39f9d88ca838d0f92154a.png

同步方法,反编译后得到ACC_SYNCHRONIZED标志

4b38244a1810e74712f3d8ecb49c2fdc.png同步代码块反编译后得到monitorenter和monitorexit指令

三、理解Java对象头

在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。

16cd3196e29b0a6d1d7b4903d220b0c7.png实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

四、 JVM对synchronized的锁优化

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁的状态总共有四种,级别从低到高依次是:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以升级但不能降级。 偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 偏向锁的获取:
  1. 判断是否为可偏向状态。
  2. 如果为可偏向状态,则判断线程ID是否是当前线程,如果是进入同步块。
  3. 如果线程ID并未指向当前线程,利用CAS操作竞争锁,如果竞争成功,将Mark Word中线程ID更新为当前线程ID,进入同步块。
  4. 如果竞争失败,等待全局安全点,准备撤销偏向锁,根据线程是否处于活动状态,决定是转换为无锁状态还是升级为轻量级锁。
当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设置为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word中,如果CAS操作成功。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。 偏向锁的释放: 偏向锁使用了遇到竞争才释放锁的机制。偏向锁的撤销需要等待全局安全点,然后它会首先暂停拥有偏向锁的线程,然后判断线程是否还活着,如果线程还活着,则升级为轻量级锁,否则,将锁设置为无锁状态。

8ec9fe497495a818e2acaaef592722ce.png轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。 自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。 重量级锁
Synchronized 的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁称之为“重量级锁”。 锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。 锁粗化
如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值