Java 虚拟机对Synchronized的优化

12 篇文章 0 订阅
12 篇文章 0 订阅

​Java虚拟机 Synchronized的优化 自旋锁,自适应的自旋锁,偏向锁,轻量级锁,锁膨胀,重量级锁

                                                                               2018年拍摄于日本京都金阁寺

微信公众号

 

王皓的GitHub:https://github.com/TenaciousDWang

 

      上一回说了Synchronized同步代码块的使用及简单原理,这一回说一下JDK1.5到JDK1.6对于Synchronized的一个重要改进,用来提高并发效率,这一回主要会介绍自旋锁,自适应的自旋锁,锁膨胀,轻量级锁,偏向锁,现在我们把传统的Synchronized定义为重量级锁。

 

    JVM的虚拟机开发团队为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

 

    自旋锁,我们前面知道Synchronized是一种互斥锁,通过对一个资源加锁保证一个线程对其执行操作,其他线程没有获取到锁时将处于阻塞状态,将线程挂起与恢复线程,每个线程都需要记录程序计数器和CPU寄存器状态,存储和恢复每个线程的执行状态,都是是资源开销,也会造成效率下降。

 

    JVM虚拟机的开发团队发现有一些共享数据被锁定的时间其实很短,为了这么短的时间去挂起和恢复线程不太值得,所以假设有一个线程在使用共享资源,第二个线程需要访问时不需要挂起线程,也就是说不放弃CPU时间片执行时间,而是等待第一个持有锁的线程是否很快的释放锁,为了让线程等待且不放弃CPU执行时间,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,这就是自旋锁。

 

    自旋等待并不能代替阻塞,首先至少两个线程都获取到CPU执行时间,首先对处理器数量是有要求的,自旋虽然避免了线程切换的开销,但是其仍然占用CPU执行时间,如果自旋时间短则效果会非常好,如果等待时间较长,那么就会长时间浪费处理器资源。

 

    在虚拟机中,自旋默认次数为10,我们可以通过设置JVM参数-XX:PreBlockSpin来更改,达到自旋次数后会按照传统方式挂起线程。

 

  自旋锁在Java 1.4.2时就引入了,默认为关闭,可以使用-XX:+UseSpinning参数来开启,Java 1.6时已经默认开启,并且引入了自适应的自旋锁,当一个线程通过自旋获取到锁后,JVM会认为其很可能再次成功,所以每次自旋获得锁时,自旋时间都会延长,相反如果一个线程很少通过自旋获取锁,则该线程以后则会被JVM忽略自旋,直接使其挂起,避免浪费资源。

 

    在说轻量锁与偏向锁,锁膨胀之前,我们需要先了解Java对象头的内存布局,这里参考了周志明《深入理解Java虚拟机》,在32位与64位的JVM虚拟机中对象头的大小分别为32bit与64bit,如果不是数组对象则分为两部分,数组对象多一个部分存储数组长度。

 

名称

内容说明

Mark Word

存储对象的hashCode或锁信息等。

Class Metadata Address

存储到对象类型数据的指针

Array length

数组的长度(如果当前对象是数组)

 

    Mark Word中所记录的锁标志位是实现轻量级锁与偏向锁的关键,接下来我们以32位JVM的Mark Word组成来看一下:32bit中有25bit用于存储对象HashCode,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,注意这些空间的大小不是固定的,会随着状态变化而发生改变,为了JVM的空间效率,在最小的空间内尽量存储更多的信息。

 

 

    在了解了Mark Word里存有锁标志位后,我们现在先了解一下锁的状态,在Java 1.6+里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

 

    首先我们来说一下偏向锁,偏向锁就是偏向第一个获取锁的线程,在无竞争状态下,同步与CAS操作都不做了,如果一直没有其他线程去获取锁,则一直偏向,偏心下去,永远不需要再进行同步。

 

    这里的CAS操作指compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

 

  • 需要读写的内存值 V
  • 进行比较的值 A(预期值,写入B之前重新获取V)
  • 拟写入的新值 B

 

    CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。一般情况下是一个自旋操作,即不断的重试。

 

    当锁对象第一次被线程获取时,JVM会把该对象头中的锁标志位改为01,即可偏向(偏向锁)状态,同时使用CAS操作将线程ID写入Mark Work之中,如果CAS操作成功,则持有该锁的线程每次进入该同步块时,就可以不做任何同步操作。

 

    当有另一个线程去获取锁对象时,根据CAS算法规则会操作失败,即写入失败,此时会根据锁对象状态来判定,如果锁对象处于未锁定状态则撤销偏向锁标记恢复为未锁定状态01,如果锁定则将锁标记设置为00,升级为轻量级锁状态。

 

    轻量级锁加锁,线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,锁记录(Lock Record),并将对象头中的Mark Word复制到锁记录中,copy的记录官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁。

 

                                                                  图片引用自周志明《进入理解JVM虚拟机》

 

 

    如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,这个就是轻量级锁,实际上为一个线程占用,另一个线程处于自旋状态,节省互斥开销。

 

    超过两个线程竞争时轻量锁不再有效,这时膨胀为重量级锁,锁标记会被JVM设置为10,进入重量锁状态,即互斥状态,其他竞争线程不再自旋而进入阻塞状态。

 

    这里的重量级锁指的就是传统synchronized同步。

 

    偏向锁与轻量级锁虽然可以提高有同步无竞争的性能,但是我们在实际工作中遇到的大多都是一个共享资源被多个线程访问,此时上述操作反而会多余并增加类似于CAS操作,自旋等资源开销,我们应该根据实际情况来使用上述优化,在锁竞争激烈的时候我们其实可以用参数-XX:-UseBiasedLocking来禁止偏向锁优化来提升性能。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值