每个对象的心中都有一把锁, 你没有对象的原因是你还没有找到那个钥匙
下面从几个方面来了解Synchronized
的用法及底层实现
- 初识
Synchronized
- 锁的本质 - 对象
- 锁的种类
- 锁升级
- 锁的本质
-
Synchronized
的JVM
字节码原语
一、初识Synchronized
官方给出的说明是
Synchronized
关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误, 如果一个对象对多个线程是可见的, 那么对该对象的所有读写都将通过同步的方式来进行.
具体表现如下
-
Synchronized
关键字提供了一种锁机制, 能够确保共享变量的互斥访问, 从而防止数据不一致的问题的出现 -
Synchronized
关键字包括monitorenter
和monitorexit
两个JVM
指令, 它能够保证在任何时候任何线程执行到monitorenter
成功之前都必须从主存获取数据, 而不是从其他缓存中; 在monitorexit
执行成功之后, 共享变量更新后的值必须刷回主存 -
Synchronized
的指令严格遵守happens-before
规则, 一个monitorexit
之前必须要有个monitorenter
Synchronized
的用法非常简单
- 可以用于方法
- 可以用于代码块
// 类锁 1
public static void show1(){
synchronized (SyncClass.class) {
// doSomeThing
}
}
// 类锁 2
public synchronized static void show2(){
// doSomeThing
}
// 对象锁 1
public void show3(){
synchronized (this) {
// doSomeThing
}
}
// 对象锁 2
public synchronized void show4(){
// doSomeThing
}
- 锁住实例方法与锁住实例字段效果一样, 锁住静态方法与锁住
Class
对象效果一样
那么锁住的是什么?
不管 synchronized (SyncClass.class)
还是 synchronized (this)
, 括号内的都是一个对象, 锁就是与这个对象关联的一个monitor
对象(又称 mutex
互斥量), 哪个线程持有这把锁, 谁就可以执行这把锁内部的同步代码
二、锁的本质 - 对象
引用<<java并发编程实战>>的一段话
每个
java
对象都可以用做一个实现同步的锁, 这些锁被称为内置锁(Intrinsic Lock
)或者监视器锁(Monitor Lock
). 线程在进入同步代码块之前会自动获得锁, 并且在退出同步代码块时自动释放锁.
获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法.
既然每个对象都能成为一把锁, 那么与锁相关的monitor
一定要有一个抽象出来的具有锁需要信息的类, 这个类就是Object
需要什么信息呢?
![](https://img.haomeiwen.com/i15085536/20870e27139e5696.png)
-
实例变量
存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐 -
填充字符
,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的
每个对象都有一个对象头
, 那么很明显, 锁需要的信息都存在这里面了.
那么对象头
里面又是什么样的呢?
所有对象都有一个对象头, 就好比一个请求都有请求头与请求体, 所有的请求其请求头的格式都是一样的, 同样的, 所有对象头的格式也是有规定的.
以下举例都是基于
32
位JVM
对象头的组成
- 普通对象头(
64bit
)
32 bits |
32 bits |
---|---|
Mark Word |
Klass Word |
- 数组对象头(
96bits
)
32 bits |
32 bits |
32 bits |
---|---|---|
Mark Word |
Klass Word |
array length |
-
Mark Word
对象头, 记录对象基本信息identity_hashcode
/age
等 -
Klass Word
指向当前对象的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 -
array length
数组对象的长度
如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(32位JVM一个字两个字节, 64位JVM一个字四个字节)
这里我们关心的就是 32 bits
的 Mark Word
部分
Mark Word< 32 bits > |
state |
---|---|
identity_hashcode:25 / age:4 / biased_lock:1 / lock:2 |
Normal |
thread:23 / epoch:2 / age:4 / biased_lock:1 / lock:2 |
Biased |
ptr_to_lock_record:30 / lock:2 |
Lightweight Locked |
ptr_to_heavyweight_monitor:30 / lock:2 |
Heavyweight Locked |
lock:2 |
Marked for GC |
上表列出了对象的五种状态及其对应的Mark Word
各部分值
-
identity_hashcode:25
25位的对象标识Hash
码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor
中 -
epoch
偏向时间戳 -
age:4
4位的GC
分代年龄,在堆中对象每活过一次GC
,即在Survivor
之间复制一次,GC年龄加一,到达一定值晋升老年代,这个值就记录在对象头的Mark Word
的age
中,age
只有4位,最大就是15,而PS + PO
组合的默认阈值就是15
,CMS
的是6
, 可通过虚拟机启动选项-XX:MaxTenuringThreshold
进行指定老年代阈值 -
lock:2
是Mark Word
的最后两位, 用于表示锁状态, 它与倒数第三位的biased_lock:1
一起判断出当前对象的状态 -
GC标记
在垃圾回收过程中涉及到对象的复制,复制过程中的对象不允许操作,这种状态就是GC 标记
biased_lock |
lock |
State |
---|---|---|
0 |
01 |
无锁 Normal |
1 |
01 |
偏向锁 Biased |
0 |
00 |
轻量级锁 Lightweight Locked |
0 |
10 |
重量级锁 Heavyweight Locked |
0 |
11 |
GC标记 Marked for GC |
剩余的两种
ptr_to_lock_record:30
和ptr_to_heavyweight_monitor:30
与对象如何在这些状态之间进行转换的在锁升级时细说
三、锁的种类
关于锁分类, 网上有一堆说法:乐观
/悲观
、内置
/显式
、自旋
/挂起
等。
其实只是从不同角度来看进行的分类,从源码实现来看我比较喜欢内置
/显式
这个组合。
加锁有几种方式
Synchronized
- 除
Synchronized
以外,比如著名的可重入锁ReentrantLock
<JDK1.5
>
Synchronized
是一种内置锁,又称监视器锁(monitor
),使用非常简单,不需要显式的获取和释放,任何一个对象都能作为一把锁,故称之为内置锁
内置锁认为短时间内根本获取不到锁,所以来抢占锁时根本不等待直接就释放自己的CPU时间片
, 这种是阻塞式等待。
而对应的显示锁是一种乐观锁,它们觉得很快就能获得锁,故在锁门口自旋等待锁释放(自旋锁
由此得名),这种响应快,但耗CPU
资源,后文会讲到其优化。
四、锁升级
对象总共有五种状态, 除GC 标记
外以下四种
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
![](https://img.haomeiwen.com/i15085536/eb03901988b77bcd.png)
再补两张图
![](https://img.haomeiwen.com/i15085536/24fc87e95648df37.png)
![](https://img.haomeiwen.com/i15085536/d11d9418a73e92a9.png)
来分别看看各个状态对象头的Mark Word
都是如何转化的.
- 创建对象 -
NORMAL
名称 | 值 |
---|---|
identity_hashcode <25> |
System.identityHashCode(Class<?>) |
age <4> |
0000 |
biased_lock <1> |
0 |
lock <2> |
01 |
- 线程
T1
进行加锁
名称 | 值 |
---|---|
thread <23> |
t1.getId() |
epoch <2> |
00 |
age <4> |
0000 |
biased_lock <1> |
1 |
lock <2> |
01 |
偏向锁不会主动释放锁,所以偏向锁的加锁有点复杂
- 加锁的过程需要进行判断
- 如果不存在偏向锁,将
identity_hashcode
复制到栈中保存并用当前线程ID与epoch
替代 - 存在偏向锁,比较当前线程ID与锁关联的对象头中线程ID是否一致
- 一致,重入值加一,无需CAS竞争直接进入同步代码执行
- 不一致,需要查看对象头中的线程ID代表线程是否存活或是否仍需要持有偏向锁
- 不存活或不需要持有,撤销偏向锁,对象置为无锁状态,当前线程进行偏向锁的获取
- 如果存活且仍需要持有此偏向锁,那么暂停当前线程,撤销偏向锁,升级为轻量级锁
- 如果不存在偏向锁,将
- 轻量级锁
轻量级锁的获取需要不同线程间的竞争,比如T1
与T2
- 虚拟机栈中新开辟一块内存
Lock-Record
- 将
Mark word
中除lock
标志位外的其他信息复制一份 - 存在虚拟机栈中的新开辟出来的
Lock-Record
中 - 然后进行
CAS
替换Mark Word
中的信息为指向Lock-Record
的指针LR-Pointer
- 成功者获取轻量级锁,失败者自旋等待
名称 | 值 |
---|---|
LR-Pointer <30> |
--- |
lock <2> |
00 |
Lock-Record
官方名称应该是DisplacedMarkWord
- 重量级锁
轻量级锁时等待线程都在自旋,如果一把锁被持有时间太久或等待线程过多,那线程自旋所消耗的CPU都是一个不容小觑的损失。
所以在满足以下条件时,轻量级锁会升级为重量级锁
- 自旋十次以上
- 自旋线程数量超过内核数量二分之一
跟轻量级锁一样前面放的都是一个指针,不过重量级锁是一个互斥量
- 生成或复用
monitor
对象 -
Mark Word
指向monitor
对象<mutex
对象> - 自旋线程进入
_EntrySet
阻塞,不在自旋让出CPU资源 - 升级重量级锁成功
名称 | 值 |
---|---|
Mutex-Pointer <30> |
--- |
lock <2> |
10 |
重量级锁由操作系统发放
锁升级基本讲完了,有几个问题
- 为什么要引入偏向锁?
其实偏向锁就是可重入锁,同一个线程不用二次争抢资源才能获取锁,这是因为很多情况下,都是同一个线程对同一把锁进行加锁、解锁,引入偏向锁就是为了减少这种“无谓”开销。 - 为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态
,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。 - 为什么要引入重量级锁?
设想一种极端情况,上亿个线程自旋等待一把轻量级锁或者一个线程等一把轻量级锁自旋上亿次,那么CPU资源都被这些自旋操作消耗完了,属于极大的浪费,就升级为重量级锁,让这些线程全部阻塞停止“无谓”的自旋。
![](https://img.haomeiwen.com/i15085536/7ba69adb1ff2e2e1.png)
注意几点
- 锁可以升级不可以降级
- 偏向锁状态可以被重置为无锁状态
- 虽然提倡锁住的代码尽可能小,减小同步代码执行时间就是减小线程等待时间,但如果在一个大代码块中存在很多小的同步代码块造成的加锁、解锁损耗也是非常可观的,此时不如用一个大的同步代码块来避免过多的加锁、解锁,这就是
锁粗化
-
JIT
在编译时通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间, 这就是锁消除
-XX:BiasedLockingStartUpDelay=0
- 偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,该选项取消了这个延迟
-XX:-UseBiasedLocking = false
- 撤销了偏向锁
五、锁的本质
现在我们应该知道了, Synchronized
是一把重量级锁.
上面说到锁的本质其实是与锁住对象相关的那个monitor
对象, 那么monitor
对象又是个什么东东呢?
![](https://img.haomeiwen.com/i15085536/a07296f1702805f9.png)
在Java虚拟机(HotSpot
)中,monitor
是由ObjectMonitor
<源码> 实现的
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
看看几个字段
-
_count
记录该对象被线程获取锁的次数 -
_recursions
锁的重入次数 -
_owner
指向持有ObjectMonitor
对象的线程 -
_WaitSet
处于wait
状态的线程,会被加入到_WaitSet
-
_EntryList
处于等待锁block
状态的线程,会被加入到该列表
![](https://img.haomeiwen.com/i15085536/7bf85b75fd9e067a.png)
通过几种操作来看看重量级锁的玩法
- 线程
T1
与线程T2
竞争执行,T1
成功,这把锁的_owner
置为T1
,T2
进入_EntryList
等待<Blocking
> - 线程
T1
执行wait()
方法释放锁,进入_WaitSet
,这把锁的_owner
置空 -
_EntryList
中线程开始竞争执行,假设T2
竞争成功,这把锁的_owner
置为T2
,其他线程继续在_EntryList
中阻塞等待 -
_WaitSet
中线程等待时间到或者被唤醒,进入_EntryList
中重新竞争 - 最后一个线程执行完成,释放锁,
_owner
置空
六、Synchronized
的 JVM
字节码原语
写段代码
public class SynchronizedClass {
public void show() {
synchronized (SynchronizedClass.class) {
System.out.println("xx");
}
}
}
只有一个同步块,反编译一下
sunyelw@windows:tst$ javap -c SynchronizedClass.class
Compiled from "SynchronizedClass.java"
public class com.hy.demo.tst.SynchronizedClass {
public com.hy.demo.tst.SynchronizedClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void show();
Code:
0: ldc #2 // class com/hy/demo/tst/SynchronizedClass
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String xx
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
sunyelw@windows:tst$
把同步相关的指令拎出来
4: monitorenter
....
14: monitorexit
...
20: monitorexit
- 一个
monitorexit
至少对一个monitorenter
- 之所以有两个
monitorexit
,是要保证哪怕执行异常也要保证锁的正常释放,这也是内部锁与显式锁最明显的区别,不用手动释放
再看看另一种写法
public class SynchronizedClass {
public synchronized void show() {
System.out.println("xx");
}
}
继续反汇编一下
public synchronized void show();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String xx
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/hy/demo/tst/SynchronizedClass;
}
- 这种直接加在方法上就没有把
JVM
指令打出来,而是在flags
上直接加了个ACC_SYNCHRONIZED
Synchronized
方法同步不再是通过插入monitorenter
和monitorexit
指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED
标志隐式实现的,如果方法表结构(method_info Structure
)中的ACC_SYNCHRONIZED
标志被设置,那么线程在执行方法前会先去获取对象的monitor
对象,如果获取成功则执行方法代码,执行完毕后释放monitor
对象,如果monitor
对象已经被其它线程获取,那么当前线程被阻塞。
参考文献
Java对象头详解
Synchronized详解
Java并发-Synchronized
今天写了一天,真的有点菜,自闭一会吃个饭再继续~~~
网友评论