Java并发——Synchronized优化(轻量级锁、偏向锁)

1 重量级锁

在上一篇博客中我们知道,Synchronized的实现依赖于与某个对象向关联的monitor(监视器)实现,而monitor是基于底层操作系统的Mutex Lock实现的,而基于Mutex Lock实现的同步必须经历从用户态到核心态的转换,这个开销特别大,成本非常高。所以频繁的通过Synchronized实现同步会严重影响到程序效率,而这种依赖于Mutex Lock实现的锁机制也被称为“重量级锁”,为了减少重量级锁带来的性能开销,JDK对Synchronized进行了种种优化,尤其是在JDK1.5中引入了轻量级锁和偏向锁。

熟悉JVM内存模型的同学都知道,每一个Java实例对象在内存中都包含一部分称之为对象头的区域,对象头记录了对象的运行时数据,这部分运行时数据包括GC分代信息和锁状态。

Java并发——Synchronized优化(轻量级锁、偏向锁)

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

2 轻量级锁

轻量级锁是相对于重量级锁而言在获取锁和释放锁时更加高效,但轻量级锁并不能代替重量级锁。轻量级锁适用的场景是在线程交替获取某个锁执行同步代码块的场景,如果出现多个进程同时竞争同一个锁时,轻量级锁会膨胀成重量级锁。

2.1 轻量级锁加锁过程

  • 当代码进入同步块时,如果同步对象锁为无锁状态(偏向锁标识为“0”,锁标志位为“01”),则当前执行线程会在当前栈帧中建立一个锁记录(Lock Record)用于复制同步对象的Mark Word, 称之为Displaced Mark Word
  • 系统通过CAS操作将对象的Mark Word更新指向Lock Word,并将同步对象的Owner Thread指定为当前线程。若果操作成功进入步骤3,失败进入步骤4
  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
  • 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程

2.2 轻量级锁的释放过程

  • 通过CAS操作将Displaced Mark Word替换对象的Mark Word
  • 如果操作成功,同步完成
  • 如果失败,则说明已经有其他线程竞争当前对象,此时对象的锁已经升级为重量级锁。则当前线程在释放的同时需要通知其他等待线程

3 偏向锁

偏向锁是基于一个经验为前提的:在多线程环境下,存在很多同步块只会被一个线程执行。在这样的情况下,使用重量级锁或者轻量锁都不是最经济的。因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。轻量级锁的机制很简单,当某个同步对象被某个线程获取后,同步对象会将其thread owner指向该线程。即使在线程退出同步块之后,同步对象的thread owner依然不会改变。当该线程下次再次进入该代码块时,不用再获取同步对象使用权而直接执行代码块。如果有其他线程竞争该同步对象,则偏向锁失效,偏向锁将升级为轻量级锁或重量级锁。

3.1 偏向锁的获取

  • 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态
  • 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)
  • 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)
  • 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码
  • 执行同步代码

3.2 偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3.3 偏向锁、轻量级锁、重量级锁之间的转换

Java并发——Synchronized优化(轻量级锁、偏向锁)

4 其他优化

4.1 适应性自旋

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

4.2 锁粗化

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。

4.3 锁消除

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

上一篇:java学习 暑假第五天


下一篇:常用经典SQL语句大全完整版--详解+实例 (存)