JAVA之锁机制实现原理(简化版)

2,701 阅读4分钟

并发编程离不开锁,然而每次遇到锁问题时都会谈锁色变,下面对锁实现作简单化描述,方便大家容易理解!

一、绪论

在JAVA中锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态(按从低到高顺序,锁着竞争情况逐渐升级)

JAVA中锁只能升级却不能降级,目的是为了提高获得锁和释放锁的效率。

二、对象头的介绍

在HopSpot虚拟机中,对象在内存存储中分为3部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。 想了解Java对象结构的详细信息请看:java对象结构

java对象头包含3部分信息,如下:

锁的状态保存在对象头的Mark Word中,以32位JDK为例:

三、锁状态的介绍

1、偏向锁

在无多线程竞争锁的情况下,为了让同一线程获得锁的代价更低而引入了偏向锁。

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

CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

1.2、偏向锁的释放

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

2、轻量级锁

2.1、轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录(Lock record)中,官方成为Displaced Mark Word。然后尝试使用CAS算法将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

2.2、轻量级锁解锁

轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前存在竞争,锁就会膨胀成重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。其他线程试图获取锁时,会被阻塞住,当持有锁的线程释放之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁竞争。

3、重量级锁

在多线程并发编程中synchronized一直是元老级角色,很多人称呼它为“重量级锁”。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,但是监视器锁本质又是依赖于底层操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换就需要从用户状态切换到核心状态,这个成本很高,状态之间转换需要相对比较长的时间,这就是synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock来实现的锁,我们称之为“重量级锁”。

在JDK1.6后,synchronized得到了种种优化后,在某些情况下已经没那么重了。

四、锁的优缺点对比

注:本文主要观点来源于《java并发编程的艺术》和个人学习网上的文章的一些总结。