文章目录
参考文章
- Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
- 聊聊并发(二)Java SE1.6中的Synchronized
- Lock Lock Lock: Enter!
- 5 Things You Didn’t Know About Synchronization in Java and Scala
- Synchronization and Object Locking
- Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
Java 中的锁
在 Java 中主要2种加锁机制:
synchronized
关键字java.util.concurrent.Lock
(Lock
是一个接口,ReentrantLock
是该接口一个很常用的实现)
这两种机制的底层原理存在一定的差别
synchronized
关键字通过一对字节码指令monitorenter/monitorexit
实现, 这对指令被 JVM 规范所描述。java.util.concurrent.Lock
通过 Java 代码搭配sun.misc.Unsafe
中的本地调用实现的
一些先修知识
先修知识 1: Java 对象头
- 字宽(Word): 内存大小的单位概念, 对于 32 位处理器 1 Word = 4 Bytes, 64 位处理器 1 Word = 8 Bytes
- 每一个 Java 对象都至少占用 2 个字宽的内存(数组类型占用3个字宽)。
- 第一个字宽也被称为对象头Mark Word。 对象头包含了多种不同的信息, 其中就包含对象锁相关的信息。
- 第二个字宽是指向定义该对象类信息(class metadata)的指针
- 非数组类型的对象头的结构如下图
- 说明:
- MarkWord 中包含对象 hashCode 的那种无锁状态是偏向机制被禁用时, 分配出来的无锁对象MarkWord 起始状态
- 偏向机制被启用时,分配出来的对象状态是 ThreadId|Epoch|age|1|01, ThreadId 为空时标识对象尚未偏向于任何一个线程, ThreadId 不为空时, 对象既可能处于偏向特定线程的状态, 也有可能处于已经被特定线程占用完毕释放的状态, 需结合 Epoch 和其他信息判断对象是否允许再偏向(rebias)。
下面的图片来自参考论文 Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing , 可以与上面的表格进行比对参照, 更为清晰, 可以看出来, 标志位(tag bits)可以直接确定唯一的一种锁状态
先修知识 2: CAS 指令
- CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。
- 该指令概念上存在 3 个参数, 第一个参数【目标地址】, 第二个参数【值1】, 第三个参数【值2】, 指令会比较【目标地址存储的内容】和 【值1】 是否一致, 如果一致, 则将【值 2】 填写到【目标地址】, 其语义可以用如下的伪代码表示。
function cas(p , old , new ) returns bool {
if *p ≠ old { // *p 表示指针p所指向的内存地址
return false
}
*p ← new
return true
}
- 注意: 该指令是是原子性的, 也就是说 CPU 执行该指令时, 是不会被中断执行其他指令的
先修知识 3: “CAS”实现的"无锁"算法常见误区
- 误区一: 通过简单应用 “比较后再赋值” 的操作即可轻松实现很多无锁算法
- CAS 指令的一个不可忽略的特征是原子性。 在 CPU 层面, CAS 指令的执行是有原子性语义保证的, 如果 CAS 操作放在应用层面来实现, 则需要我们自行保证其原子性。 否则就会发生如下描述的问题:
// 下列的函数如果不是线程互斥的, 是错误的 CAS 实现
function cas( p , old , new) returns bool {
if *p ≠ old { // 此处的比较操作进行时, 可以同时有多个线程通过该判断
return false
}
*p ← new // 多个线程的赋值操作会相互覆盖, 造成程序逻辑的错误
return true
}
- 误区二: CAS 操作的 ABA 问题
- 大部分网络博文对 ABA 问题的常见描述是: 应用 CAS 操作时, 目标地址的值刚开始为 A, 工作线程/进程 读取后, 进行了一系列运算, 计算得出了新值 C, 在此期间, 目标地址的值被其他线程已经进行了不止一次修改, 其值已经从 A 被改为 B , 又改回 A, 此时便会发生同步问题。
- 上面的描述是其实是错误的, 思考一下就会发现, 如果工作线程的操作目的是将目标地址的值从 A 改为 C, 那么即便在这期间目标地址的值经过了其他线程或进程的多次修改, 其语义依旧是正确的。
- 例如目前要将某银行账号的余额扣除 50, 通过 CAS 保证同步 :
- 首先读取原有余额为 100 ,
- 计算余额应该赋值为 100 - 50 = 50
- 此时该线程被挂起, 该账户同时又发生了转入 150 和转出 150 的操作, 余额经历了 100 -》250 -》100 的变动
- 线程被唤醒, 进行 CAS 赋值操作 cas(p, 100, 50) , 正常得以执行。
- 该账户的余额依旧是正确的
- 通过上述例子就可以发现, ABA 的问题并不在于多次修改。 查阅一下 CAS 的 wiki 解释, 就会发现, ABA 真正的问题是, 假如目标地址的内容被多次修改以后, 虽然从二进制上来看是依旧是 A, 但是其语义已经不是 A 。例如, 发生了整数溢出, 内存回收等等。
先修知识 4: 栈帧(Stack Frame) 的概念
- 这个概念涉及的内容较多, 不便于展开叙述。 从理解下文的角度上来讲, 需要知道, 每个线程都有自己独立的内存空间, 栈帧就是其中的一部分。里面可以存储仅属于该线程的一些信息。
- 需要深入了解的同学, 需要自行查阅 栈帧 相关的概念
先修知识 5: 轻量级加锁的过程
轻量级加锁的过程在参考文章一中有较为的描述以及配图, 这里直接将其摘抄过来, 做轻微整理和调整
- (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态”, 是否可偏向的标志位是 “1”), 也就是如下状态
,然后, 虚拟机会首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
-
(2)拷贝对象头中的Mark Word复制到锁记录中。这时候线程堆栈与对象头的状态如图2.1所示
-
图 2.1
-
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。
-
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
-
图2.2
-
5)如果这个更新操作失败了,说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁
先修知识 6: 重量级加锁的过程
- 轻量级锁在向重量级锁膨胀的过程中, 一个操作系统的互斥量(mutex)和条件变量( condition variable )会和这个被锁的对象关联起来。
- 具体而言, 在锁膨胀时, 被锁对象的 markword 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针
synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)
前面提到过, synchronized 代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制
- 偏向锁(Biased Lock )
- 轻量级锁( Lightweight Lock)
- 重量级锁(Heavyweight Lock)
上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。
注意 JVM 提供了关闭偏向锁的机制, JVM 启动命令指定如下参数即可
-XX:-UseBiasedLocking
下图展现了一个对象在创建(allocate) 后, 根据偏斜锁机制是否打开, 对象 MarkWord 状态以不同方式转换的过程
上图在参考文章一中的中文翻译对照图如下
无锁 -> 偏向锁
从上图可以看到 , 偏向锁的获取方式是将对象头的 MarkWord 部分中, 标记上线程ID, 以表示哪一个线程获得了偏向锁。 具体的赋值逻辑如下:
- 首先读取目标对象的 MarkWord, 判断是否处于可偏向的状态(如下图)
下面是 Open Jdk/ JDK 8 源码 中检测一个对象是否处于可偏向状态的源码