synchronized读书笔记
1. 认识synchronized
synchronized 是 Java 并发模块 非常重要的关键字,它是 Java 内建的一种同步机制,
代表了某种内在锁定的概念当一个线程对某个共享资源加锁后,其他想要获取共享资源的线程必须进行等待.
synchronized 也具有互斥和排他的语义。通过独占某个资源以达到互斥、排他的目的。
2. synchronized使用
- synchronized 修饰实例方法,相当于是对类的实例进行加锁, 即锁是new出来的当前实例对象
- synchronized 修饰静态方法,相当于是对类对象(即类的class对象)进行加锁;
- synchronized 修饰代码块,相当于是给对象进行加锁(锁是括号里面的对象),在进入代码块前需要先获得对象的锁。
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码
2.1 修饰实例方法(普通方法)
synchronized 修饰实例方法,实例方法是属于类的实例。
synchronized 修饰的实例方法相当于是对象锁。
public synchronized void method(){ }
public class TSynchronized implements Runnable{
static int i = 0;
public synchronized void increase(){
i++;
System.out.println(Thread.currentThread().getName());
}
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
TSynchronized tSynchronized = new TSynchronized();
Thread aThread = new Thread(tSynchronized);
Thread bThread = new Thread(tSynchronized);
aThread.start();
bThread.start();
//thread.join()表示等待这个线程处理结束。
//这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性。
aThread.join();
bThread.join();
System.out.println("i = " + i);
}
}
//result:上面输出的结果 i = 2000 ,并且每次都会打印当前线程的名字
解释: ,代码中的 i 是一个静态变量,静态变量也是全局变量,静态变量存储在方法区中。
increase 方法由 synchronized 关键字修饰,但是没有使用 static 关键字修饰,表示 increase 方法是一个实例方法,每次创建一个 TSynchronized 类的同时都会创建一个 increase 方法,increase 方法中只是打印出来了当前访问的线程名称。
Synchronized 类实现了 Runnable 接口,重写了 run 方法,run 方法里面就是一个 0 - 1000 的计数器,这个没什么好说的。
在 main 方法中,new 出了两个线程,分别是 aThread 和 bThread,Thread.join 表示等待这个线程处理结束。这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性
public class SynchronizedTest {
public synchronized void method1(){
System.out.println("Method 1 start");
try {
System.out.println("Method 1 execute");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public synchronized void method2(){
System.out.println("Method 2 start");
try {
System.out.println("Method 2 execute");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.method2();
}
}).start();
}
}

结果: 线程2需要等待线程1的method1执行完成才能开始执行method2方法
2.2 修饰静态方法
synchronized 修饰静态方法就是 synchronized 和 static 关键字一起使用
当 synchronized 作用于静态方法时,表示的就是当前类的锁,
因为静态方法是属于类的,它不属于任何一个实例成员,因此可以通过 class 对象控制并发访问。
public static synchronized void increase(){}
这里需要注意一点,因为 synchronized 修饰的实例方法是属于实例对象, 而 synchronized
修饰的静态方法是属于类对象,所以调用 synchronized 的实例方法并不会阻止访问 synchronized 的静态方法。
public class SynchronizedTest {
public static synchronized void method1(){
System.out.println("Method 1 start");
try {
System.out.println("Method 1 execute");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public static synchronized void method2(){
System.out.println("Method 2 start");
try {
System.out.println("Method 2 execute");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
final SynchronizedTest test2 = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test2.method2();
}
}).start();
}
}
执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法)。
所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1和method2,不能并发执行。

2.3 修饰代码块
下面代码中将 obj 作为锁对象对其加锁,每次当线程进入 synchronized 修饰的代码块时就会要求当前线程持有obj 实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。
synchronized 修饰的代码块,除了可以锁定对象之外,也可以对当前实例对象锁、class 对象锁进行锁定
public void run() {
synchronized(obj){
for(int j = 0;j < 1000;j++){
i++;
}
}
}
// 实例对象锁
synchronized(this){
for(int j = 0;j < 1000;j++){
i++;
}
}
//class对象锁
synchronized(TSynchronized.class){
for(int j = 0;j < 1000;j++){
i++;
}
}
ublic class SynchronizedTest {
public void method1(){
System.out.println("Method 1 start");
try {
synchronized (this) {
System.out.println("Method 1 execute");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public void method2(){
System.out.println("Method 2 start");
try {
synchronized (this) {
System.out.println("Method 2 execute");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.method2();
}
}).start();
}
}
执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。

3. 浅析原理
同步代码块是使用monitorenter和monitorexit指令实现的,
同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。
先需要了解两个重要的概念:Java对象头,Monitor。
3.1 Java对象内存布局
可先阅读该篇文章JVM中对象描述
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
这三块区域的内存分布如下图所示:

3.2对象头 Header
synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?
Hotspot虚拟机的对象头主要包括三部分数据:
- Mark Word(标记字段) 存对象的hashCode或锁信息
- Klass Pointer(类型指针)存储 到对象类型数据的指针
- Array length 数组长度(只有当前对象是数组才有)
Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键
注:下面的内容指的是64位的VM


在上面的虚拟机对象头分配表中,我们可以看到有几种锁的状态:无锁(无状态),偏向锁,轻量级锁,重量级锁,其中轻量级锁和偏向锁是 JDK1.6 中对 synchronized 锁进行优化后新增加的,其目的就是为了大大优化锁的性能,所以在 JDK 1.6 中,使用 synchronized 的开销也没那么大了。其实从锁有无锁定来讲,还是只有无锁和重量级锁,偏向锁和轻量级锁的出现就是增加了锁的获取性能而已,并没有出现新的锁。
从图中可以看到,在重量级锁时,对象头的锁标记位为 10,并且会有一个指针指向这个 monitor 对
象,所以锁对象和 monitor 两者就是这样关联的。(下方会介绍几种锁的区别,但monitor是与重量级锁关联, 所以下面介绍monitor的锁时即指重量级锁)
而这个 monitor 在 HotSpot 中是 c++ 实现的,叫 ObjectMonitor,它是管程的实现,也有叫监视器的。
啥是 Monitor 对象呢?
3.3 Monitor 对象(重量级锁时) (翻译叫监视器或者管程)
任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制。 管程是一种同步原语,在 Java 中指的就是 synchronized,可以理解为 synchronized 就是 Java 中对管程的实现。
管程提供了一种排他访问机制,这种机制也就是 互斥。互斥保证了在每个时间点上,最多只有一个线程会执行同步方法。
所以你可以理解为 Monitor 对象其实就是使用管程控制同步访问的一种对象。
下图是monitor对象包含的内容: 先了解一下概念

这段 C++ 中需要注意几个属性:_cxp、 _WaitSet 、 _EntryList 和 _Owner,每个等待获取锁的线程都会被封装称为 ObjectWaiter 对象。
线程在任何时间最多出现在一个列表上:要么是cxp, 要么在EntryList要么在waitSet
- _cxp: 多个线程争抢锁会先到这个列表中(多线程竞争锁进入时的单向链表)
- _EntryList : 这个列表也是存争抢锁失败的锁
- _WaitSet: 调用wait()方法的线程, 也就是说处于wait状态的线程 会存在这个里面
- _owner: 标识拥有锁的线程,若无则为null
- _count: 线程获取锁的次数
- _recursions :线程的重入次数
- _object: 存储的锁对象(比如synchronized括号里的对象)
为什么会有_cxq 和 _EntryList 两个列表来放线程?
因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 存住这些并发,然后另外搞一个 _EntryList 这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。

3.4 Synchronized 修饰代码块时
此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,enter 就是要获得锁了,exit 就是要解
锁了,与之对应的就是获得锁和解锁。


从上图来看,执行 System.out 之前执行了 monitorenter 执行,这里执行争锁动作,拿到锁即可进入调用完之后有个 monitorexit 指令,表示释放锁
图中还标了一个 monitorexit 指令时,因为有异常的情况也需要解锁,不然就死锁了
从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁?
编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了。
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
monitorenter指令(争锁)

monitorexit指令(解锁)

调用 wait 的方法
就是将当前线程加入到 _waitSet 这个双向链表中,
然后再执行ObjectMonitor::exit 方法来释放锁。
调用 notify 的方法
就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。
引入自旋
synchronized 的原理大致应该都清晰了,我们也知道了底层会用到系统调用,会有较大的开销,那思考一下该如何优化?
从小标题就已经知道了,方案就是自旋
自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放。
正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。
所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。
但是自旋的次数又是一个难点,在竞争很激烈的情况,自旋就是在浪费 CPU,因为结果肯定是自旋一会让之后阻塞。
所以 Java 引入的是自适应自旋,根据上次自旋次数,来动态调整自旋的次数,这就叫结合历史经验做事。
注意这是重量级锁的步骤,别忘了前面开头说的。
总结:
synchronized 底层是利用 monitor 对象,CAS 和 mutex 互斥锁来实现的,内部会有等待队列(cxq 和 EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。
未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。
然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。
所以又引入了自适应自旋机制,来提高锁的性能。
3.5 Synchronized 修饰方法时
修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。
此时字节码中没有 monitorenter 和 monitorexit 指令,不过在当前方法的访问标记上做了手脚。
原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,
这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。
3.6 锁优化

我们再思考一下,是否有这样的场景:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。
在锁竞争不激烈的情况下,这种场景还是很常见的,可能是常态,所以轻量级锁的引入很有必要。
在介绍轻量级锁的原理之前,再看看之前 MarkWord 图。

3.6.1 轻量级锁
轻量级入锁操作的就是对象头的 MarkWord 。
- 如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw里。
然后通过 CAS 把锁对象头指向这个 LockRecord 。 - 如果当前是有锁状态,并且是当前线程持有的,则将 null 放到 dhw 中,这是重入锁的逻辑。

轻量级解锁操作
就是要把当前栈帧中 LockRecord 存储的 markword (dhw)通过 CAS 换回到对象头中。
如果获取到的 dhw 是 null 说明此时是重入的,所以直接返回即可,否则就是利用 CAS 换,如果 CAS 失败说明此时有竞争,那么就膨胀!
注: 每次加锁肯定是在一个方法调用中,而方法调用就是有栈帧入栈,如果是轻量级锁重入的话那么此时入栈的栈帧里面的 dhw 就是null,否则就是锁对象的 markword。
这样在解锁的时候就能通过 dhw 的值来判断此时是否是重入的。
3.6.2 偏向锁
场景:一开始一直只有一个线程持有这个锁,也不会有其他线程来竞争,此时频繁的 CAS 是没有必要的,CAS 也是有开销的。
所以 JVM 研究者们就搞了个偏向锁,就是偏向一个线程,那么这个线程就可以直接获得锁。
我们再看看这个图,偏向锁在第二行

如果当前锁对象支持偏向锁,那么就会通过 CAS 操作:将当前线程的地址(也当做唯一ID)记录到 markword 中,并且将标记字段的最后三位设置为 101。
之后有线程请求这把锁,只需要判断 markword 最后三位是否为 101,是否指向的是当前线程的地址。
还有一个可能很多文章会漏的点,就是还需要判断 epoch 值是否和锁对象的类中的 epoch 值相同。
如果都满足,那么说明当前线程持有该偏向锁,就可以直接返回。
这 epoch 干啥用的?
可以理解为是第几代偏向锁。 偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。 而当一类对象撤销的次数过多,比如有个
Tets 类的对象作为偏向锁,经常被撤销,次数到了一定阈值 (XX:BiasedLockingBulkRebiasThreshold,默认为
20 )就会把当代的偏向锁废弃,把类的 epoch 加1所以当类对象和锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。
不过为保证正在执行的持有锁的线程不能因为这个而丢失了锁,偏向锁撤销需要所有线程处于安全点,然后遍历所有线程的 Java栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch值加 1。
当撤销次数超过另一个阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为40),则废弃此类的偏向功能,也就是说这个类都无法偏向了。
3.7 小总结
轻量级锁与偏向锁类似,都是jdk对于多线程的优化,不同的是轻量级锁是通过CAS来避免开销较大的互斥操作,而偏向锁是在无资源竞争的情况下完全消除同步。
轻量级锁的“轻量”是相对于重量级锁而言的,它的性能会稍好一些。轻量级锁尝试利用CAS,在升级为重量级锁之前进行补救,目的是为了减少多线程进入互斥,当多个线程交替执行同步块时,jvm使用轻量级锁来保证同步,避免线程切换的开销,不会造成用户态与内核态的切换。但是如果过度自旋,会引起cpu资源的浪费,这种情况下轻量级锁消耗的资源可能反而会更多。
引入偏向锁的目的:
在只有单线程执行情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取及释放依赖多次CAS
原子指令,而偏向锁只依赖一次CAS原子指令置换ThreadID,之后只要判断线程ID为当前线程即可,
偏向锁使用了一种等到竞争出现才释放锁的机制,消除偏向锁的开销还是蛮大的。
如果同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的,可以通过-
XX:-UseBiasedLocking=false来关闭
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和核心态转换),但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁重入:对于不同级别的锁都有重入策略,偏向锁:单线程独占,重入只用检查threadId等于该线程;轻量级锁:重入将栈帧中lock record的header设置为null,重入退出,只用弹出栈帧,直到最后一个重入退出CAS写回数据释放锁;重量级锁:重入_recursions++,重入退出_recursions–,_recursions=0时释放锁
本文详细解读了synchronized关键字在实例方法、静态方法和代码块中的应用,探讨了Java对象内存布局、Monitor对象和锁的优化,包括轻量级锁、偏向锁及其性能提升策略。
1147





