1、前言
起因
虽然网络上不乏各种大佬,讲解
synchronized
的文章更是数不胜数,但是看一千遍不如做一遍,我希望将知识转化成自己的 (因此有了本文)。
目的:
本文尝试通俗易懂的讲清楚
synchronized
,我们将从线程安全讲起,引出synchronize
的使用,然后对synchronized
的字节码进行分析,然后是讲解Objectmonitor,再到各种锁(轻量级锁,偏向锁,重量级锁)的讲解,最后我们来个总结。希望和各位读者一起学习,进步!
2、多线程的利与弊&如何解决
2.1、使用多线程编程的优缺讨论
多线程编程在我们实际coding中,是很常见的一种编程方式,其 优点
显而易见,但任何事物都有两面性,他也有缺点
,优/缺 点如下:
-
优
- 充分利用现代机器的
多核cpu
,使得资源利用最大化 - 多线程的处理方式,一般是要快于单线程的(当然特殊情况除外)
- 从使用者角度来讲,系统响应速度更快,体验更好
- ......
- 充分利用现代机器的
-
缺
- 多个线程共享的资源,可能会出现线程安全问题
- 线程间切换,也是会消耗一定资源的
- 程序复杂性增加,编写以及排查时,需要一定成本
- 可能发生一些问题,如(死锁,cpu超过100% 等等异常现象)
- ......
其中缺点 2
是使用多线程必然存在且基本无法解决的问题,而缺点 3
和缺点 4
在于开发者的水平和经验了。
而缺点 1
是使用多线程时候最常见的问题,并且也是必须要解决的,如果保证不了线程安全
,那么不管你编写的代码,多么的快,多么的优雅,也将是三个字 :白忙活
,因为大部分情况下 数据准确是必须要的!
在保证数据准确的情况下,再谈代码的效率,速度等等内容,否则一切都是徒劳!
那么到底什么是线程安全,?为什么多线程场景下会出现线程安全问题?(虽然很多小伙伴知道这些已经被人们说烂了的概念,但是为了文章完整性,我们这里还是有必要说一下)下边让我们举个 🌰说明一下:
2.2、何为线程安全?如何保证?
我们一句话概括什么是线程安全:
当多个线程同时 对同一个共享资源进行非原子操作(如 :修改某个共享资源的数据时),将会出现线程安全问题。
Talk is cheap. Show me the code. 请看如下代码:
public class ThreadUnsafeTestByPool {
//我的cpu颗数: 12
private static int cpuCount = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
static {
System.out.println("我的cpu颗数: "+cpuCount);
threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
}
//成员变量,可被多个线程 共享
private static long n = 0L;
public static void main(String[] args) throws Exception {
//多弄点,不然不好看出结果
CountDownLatch countDownLatch = new CountDownLatch(10000);
for (int i = 1; i < 10001; i++) {
threadPool.execute(() -> {
try {
//synchronized (ThreadUnsafeTestByPool.class) {
n++;
//}
//System.out.println("当前提交的线程: "+Thread.currentThread().getName());
} finally {
countDownLatch.countDown();
}
});
}
//等待所有线程 执行完后再打印 ++的结果
countDownLatch.await();
System.out.println("最终++的结果: " + n);
}
}
第一次运行控制台输出:
最终++的结果: 9961
第二次运行控制台输出:
最终++的结果: 9933
第三次运行控制台输出:
最终++的结果: 9970
第n次
... 总之基本上没有算对的、
-------------------------------------------------------------------------------------------
而加上 synchronized 代码块后
第1次
最终++的结果: 10000
第2次
最终++的结果: 10000
第三次
最终++的结果: 10000
每次都运行无误
上边 代码很简单,即:使用 多个线程
对成员变量n
进行 10000次 ++
操作。
我们要的结果是不管多少个线程,只要是++了一万次,那么n必须等于10000,但是如果我们不给++操作加锁,那么结果总是不尽人意,当我们给++操作加上synchronized
后,发现每次的结果很准确都是10000。
所以我们得出结论: 给有线程安全问题的代码/逻辑 加上锁后
,能保证锁中逻辑是同步的
,写入/读取操作是安全的
,同一时刻,只有一个线程可以执行锁中逻辑从而避免了线程安全问题。
ps: 锁的实现有很多种,我们不做其他介绍,在这里只一心一意的说 synchronized
; 另外值得注意的是:加锁是保证线程安全的方式之一,并不是唯一方法,比如ThreadLocal
你看它哪里加锁了?人家也可以保证线程安全的,由于本文主要围绕synchronized
展开,所以对其他能保证线程安全的方式 不做展开
。
3、synchronized
的使用与反汇编观察
接下来我们看下 synchronized
使用的几种 姿势
3.1、修饰成员方法
- 当
synchronized
修饰成员方法
时,其使用的锁对象是 当前方法所在类的实例
,也就是this
,代码如下:
public class SyncWorkOnMemberMethodTest {
private static int cpuCount = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
static {
System.out.println("我的cpu颗数: " + cpuCount);
threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
}
//成员变量,可被多个线程 共享
private static long n = 0L;
public static void main(String[] args) throws Exception {
//多弄点,不然不好看出结果
CountDownLatch countDownLatch = new CountDownLatch(10000);
//一定要在线程外new 该对象,保证多个线程竞争的是唯一一把锁,从而达到 "互斥"
SyncWorkOnMemberMethodTest lockObj = new SyncWorkOnMemberMethodTest();
for (int i = 1; i < 10001; i++) {
//注意我们不能在for循环中new 这个对象,因为那样的话锁就不是唯一的了,也就无法保证多个线程去竞争一把锁,达不到 "互斥" 的效果
//SyncWorkOnMemberMethodTest lockObj = new SyncWorkOnMemberMethodTest();
threadPool.execute(() -> {
lockObj.incr(countDownLatch);
});
}
//等待所有线程 执行完后再打印 ++的结果
countDownLatch.await();
System.out.println("最终++的结果: " + n);
}
//修饰成员方法,其使用的锁对象是 当前类所在的实例对象(或者说当前方法的调用对象),也就是 this
private synchronized void incr(CountDownLatch countDownLatch) {
try {
n++;
} finally {
countDownLatch.countDown();
}
}
}
修饰成员方法 反汇编后得到的代码:
//略
private synchronized void incr(java.util.concurrent.CountDownLatch);
descriptor: (Ljava/util/concurrent/CountDownLatch;)V
flags: (0x0022) ACC_PRIVATE, ACC_SYNCHRONIZED
Code:
stack=4, locals=3, args_size=2
0: getstatic #15 // Field n:J
3: lconst_1
4: ladd
5: putstatic #15 // Field n:J
8: aload_1
9: invokevirtual #19 // Method java/util/concurrent/CountDownLatch.countDown:()V
12: goto 22
15: astore_2
16: aload_1
17: invokevirtual #19 // Method java/util/concurrent/CountDownLatch.countDown:()V
20: aload_2
21: athrow
22: return
//略
可以看到 被synchronized修饰的成员方法
,有一个 ACC_SYNCHRONIZED
标记。这里我们先记住,后续分析原理时,我们再深入说。
3.2、修饰静态方法
- 当
synchronized
修饰静态方法
时,使用的锁对象是当前类
,代码如下:
public class SyncWorkOnStaticsMethodTest {
private static int cpuCount = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
static {
System.out.println("我的cpu颗数: " + cpuCount);
threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
}
//成员变量,可被多个线程 共享
private static long n = 0L;
public static void main(String[] args) throws Exception {
//多弄点,不然不好看出结果
CountDownLatch countDownLatch = new CountDownLatch(10000);
for (int i = 1; i < 10001; i++) {
threadPool.execute(() -> {
incr(countDownLatch);
});
}
//等待所有线程 执行完后再打印 ++的结果
countDownLatch.await();
System.out.println("最终++的结果: " + n);
}
//修饰静态方法,使用的锁对象是当前类
private static synchronized void incr(CountDownLatch countDownLatch) {
try {
n++;
} finally {
countDownLatch.countDown();
}
}
}
修饰静态方法 反汇编得到的代码:
//略
private static synchronized void incr(java.util.concurrent.CountDownLatch);
descriptor: (Ljava/util/concurrent/CountDownLatch;)V
flags: (0x002a) ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=4, locals=2, args_size=1
0: getstatic #13 // Field n:J
3: lconst_1
4: ladd
5: putstatic #13 // Field n:J
8: aload_0
9: invokevirtual #17 // Method java/util/concurrent/CountDownLatch.countDown:()V
12: goto 22
15: astore_1
16: aload_0
17: invokevirtual #17 // Method java/util/concurrent/CountDownLatch.countDown:()V
20: aload_1
21: athrow
22: return
//略
可以看到 被synchronized
修饰的静态方法,也有一个 ACC_SYNCHRONIZED
标记。这里我们先记住,后续分析原理时,我们再深入说。
3.3、修饰代码块
- 当
修饰代码块
时候,使用的锁对象是给定的 类/对象 ,代码如下:
public class SyncWorkOnObjTest {
private static int cpuCount = Runtime.getRuntime().availableProcessors();
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
static {
System.out.println("我的cpu颗数: " + cpuCount);
threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
}
//成员变量,可被多个线程 共享
private static long n = 0L;
public static void main(String[] args) throws Exception {
//多弄点,不然不好看出结果
CountDownLatch countDownLatch = new CountDownLatch(10000);
SyncWorkOnObjTest syncWorkOnObjTest = new SyncWorkOnObjTest();
for (int i = 1; i < 10001; i++) {
threadPool.execute(() -> {
syncWorkOnObjTest.incr(countDownLatch);
});
}
//等待所有线程 执行完后再打印 ++的结果
countDownLatch.await();
System.out.println("最终++的结果: " + n);
}
//在方法中,构成了 synchronized块的, 使用的锁对象是括号中的 类/对象
private void incr(CountDownLatch countDownLatch) {
try {
synchronized (countDownLatch){
n++;
}
} finally {
countDownLatch.countDown();
}
}
}
修饰代码块 反汇编后得到的代码:
- 注意,由于这里需要看懂部分
汇编指令
,所以我们在关键部分加上注释
,好知道这个汇编指令是干了什么。
//略
private void incr(java.util.concurrent.CountDownLatch);
descriptor: (Ljava/util/concurrent/CountDownLatch;)V
flags: (0x0002) ACC_PRIVATE
Code:
stack=4, locals=5, args_size=2
0: aload_1
1: dup
2: astore_2
3: monitorenter //获取锁
4: getstatic #15 //从类中获取静态字段 // Field n:J
7: lconst_1 //将long类型常量1压入栈
8: ladd //执行long类型的加法
9: putstatic #15 //设置类中静态字段的值 // Field n:J
12: aload_2
13: monitorexit //释放锁
14: goto 22
17: astore_3
18: aload_2
19: monitorexit //确保在发生异常时候,也释放monitor 锁
20: aload_3
21: athrow
22: aload_1
23: invokevirtual #19 //调用对象的实例方法:invokevirtual // Method java/util/concurrent/CountDownLatch.countDown:()V
26: goto 38
29: astore 4
31: aload_1
32: invokevirtual #19 // Method java/util/concurrent/CountDownLatch.countDown:()V
35: aload 4
37: athrow
38: return //从方法中返回,返回值为void
//略
可以看到被synchroized
修饰的代码块, 在进行n++
之前,会先执行 monitorenter
汇编指令来获取锁
,然后是get put (也就是++操作)然后是 monitorexit
指令,此时,锁被当前持有的线程释放
值的注意的是: 当锁是某个对象实例时,一定要保证,该实例的
唯一性
,否则多个线程去竞争n个锁,是无法保证“互斥” 的特性的,只有锁对象唯一(也就是只new了一次,或者是一个class)才能多个线程去抢同一把锁,从而达到“互斥”的目的
4、到底什么是真正的锁资源?
我们在第三节
给出了syncroized
的使用方式以及反汇编
后得到的内容分析,这里再次回顾下:
1. 被`synchronized`修饰的**成员方法**,有一个 `ACC_SYNCHRONIZED` 标记。
2. 被`synchronized`修饰的**静态方法**,也有一个 `ACC_SYNCHRONIZED` 标记。
3. 被`synchroized`修饰的**代码块**, 再进行代码块里的逻辑 之前,会先执行 `monitorenter`汇编指令来
`获取锁`,执行逻辑完毕后,紧接着是 `monitorexit`指令,此时,锁被当前持有的线程`释放`
那么,给方法打上 ACC_SYNCHRONIZED
标记,以及执行monitorenter
和monitorexit
指令,底层到底会做哪些操作呢?我们接着看~~
关于monitorenter指令的处理,在github.com/JetBrains/j… 文件中里边可以看到:
- 如果开启偏向锁,就执行
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);(偏向锁)
- 否则就执行
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);(轻量级锁)
但是,注意: 在第4节,我们其实主要是在讲ObjectMonitor,而这个东西是在锁膨胀为 重量级锁时候才会使用到的,所以在下边的 4.1、 4.2、 4.3 小节中,我们都假定 锁已经膨胀为重量级。这一点要特别清楚。(同时应该知道的是:在jdk1.5(包含)之前,synchroized只有一种情况就是直接使用重量级锁,在之后的版本中,才有偏向,轻量级 这两个锁级别)
4.1、真正的资源:ObjectMonitor
实际上不管是被标记为 ACC_SYNCHRONIZED
的同步方法 还是 被插入monitorenter/monitorexit
指令的同步块 ,最终JVM的底层都是一个逻辑,那就是:
- 在进入该方法或同步块时,必须 竞争到 给定对象 对应的ObjectMonitor上的资源,
(这里的 "竞争" 具体表现为:
某线程通过CAS操作来将给定锁对象对应的ObjectMonitor 上的 owner指针 指向自己
,CAS(具体怎么cas下边会说)操作成功,就代表获取锁成功,CAS失败,就代表未获取到锁)。 同时,我们要必须清楚的一点是,每一个对象 都有一个ObjectMonitor与之相对应,且唯一。这里给出 官方 对monitorenter的解释 来源点这里:每个对象都与一个监视器相关联。 当且仅当监视器与某个对象相关联时,它才会被锁定。 执行 monitorenter 的线程尝试获得与 对象 关联的监视器的所有权,如下所示:
- 如果与 对象 关联的监视器的条目(count值)计数为零(说实话我在源码中找半天也没看见count++的代码),则线程进入监视器并将其条目(count)计数设置为一。 线程就是监视器的所有者。
- 如果线程已经拥有与 对象 关联的监视器,它会重新进入监视器,增加其count计数。
- 如果另一个线程已经拥有与 锁对象 关联的监视器,线程将阻塞直到监视器的count计数为零,然后再次尝试获得所有权。
- (个人补充:)我觉得最重要的一点: 抢锁的操作对应到JVM的底层来说其实就是CAS设置owner指针指向当前抢锁线程的操作,设置成功即抢锁成功,设置失败即抢锁失败。
紧接着我们看下hotspot中的ObjectMonitor 长啥样:
下边是 hotspot源码 /src/hotspot/share/runtime/objectMonitor.hpp
类中的部分片段,该片段定义了objectMonitor
类的一些组成
,其中有几个比较重要的成员我们会介绍一下,以便下边更好理解
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
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;
}
对主要字段作下说明 :
_owner: 指向当前持有锁的Thread。所谓的加锁就是抢锁线程通过
CAS
把ObjectMonitor
的Owner指向自己,ObjectMonitor中的Owner指向谁谁就持有锁,为null时就是空闲状态。_WaitSet: 用来存放那些因调用了wait()方法而被阻塞到当前Monitor的线程。以便于调用notify时进行唤醒操作。这也是为什么同步代码块一旦调用wait/notify就会直接进入重量级锁模式(因为使用到了ObjectMonitor就意味着锁已经升级到重量级),以及wait/notify需要在同步代码块内执行,否则会抛出IllegalMonitorStateException的原因。
_cxq: 存放那些获取锁失败后进入阻塞状态等待锁资源释放的线程。通过CAS操作放在cxq的首部。
EntryList: 用于记录阻塞在当前Monitor上的线程
_recursions: 锁的重入次数
_count: 获取锁成功后会+1
是不是很蒙蔽?cxq和Entry List作用好像一样呀,都是存放等着抢锁的线程,其实是有点区别的,我们简单描述:cxq是预备区,优先级更低 Entry List是排队区,优先级更高些,关于为啥这么设计,欢迎评论区留言~~
4.2、锁对象与ObjectMonitor的关系
我自己对 对象和ObjectMonitor 关系的理解:
- 执行
monitorenter
的线程试图获得与指定对象(synchronized块中的锁对象) 关联的ObjectMonitor
的所有权,每个对象都有一个监视器(ObjectMonitor)与之相关联。 - 当且仅当监视器(ObjectMonitor)和某个对象产生关联时(也就是说该对象的
markword中指向互斥量monitor的指针: ptr_to_heavyweight_monitor 指向某个ObjectMonitor时
,(或者从线程角度来说)ObjectMonitor
中的owner
指针指向当前线程id
时候),ObjectMonitor
对象标识为已被锁定/持有。 - 基于上边这两条理解,我画了一张图
(这里我们假设的场景是已经升级为重量级锁,如果不是重量级锁,也不存在ptr_to_lock_record指针,更不会用到ObjectMonitor,这一点我们要非常清楚)
,这里给出来使得脑海中更清楚这个关系,如下: - 从上边(1、2、3) 不难得出 结论(这里我们只讨论重量级锁的情况):
所谓的锁资源(或者叫互斥量),其实并不是某个对象,而是与锁对象对应的 ObjectMonitor,多线程之间抢的资源,其实是
给定锁对象
对应的ObjectMonitor
中的owner
当owner指向哪个线程,哪个线程就持有锁! 抢不到锁的统统靠边站!
下边我们通过源码来看看ObjectMonitor到底做了什么~~
4.3、ObjectMonitor图解
我们在上边说了,线程真正抢的资源是ObjectMonitor,那么是怎么抢的呢?抢成功了的线程怎么处理?抢失败了的线程怎么处理?
一图胜千言,先上图,再解释。ObjectMonitor流转图如下:
对上图做个解释:
- 尝试获取: 当线程尝试获取锁时,会进行cas操作尝试将owner指针 指向当前线程,如果获取锁成功,则进入同步块执行逻辑
- 获取失败后: 从cxq队首插入,包装了当前线程的node
- 当持有锁的线程释放锁后: 首先肯定的就是他会将owner置位null,好让出资源,然后会从EntryList(如果没有从cxq)队列中挑选一个线程进行抢锁,被选中的线程叫做Heir presumptive即假定继承人也叫(
OnDeck
),假定继承人(OnDeck)
尝试获得锁,但synchronized
是非公平的,所以假定继承人(OnDeck)
不一定能获得锁(这也是它叫"假定"继承人的原因)。 - 当持有锁的线程调用Object的wait方法后: 则会将
当前线程
加入到WaitSet
队列中, - 当被
Object#notify/notifyAll
方法唤醒后: 会将对应线程从WaitSet
移动到cxq或EntryList
中去(具体移动策略有点复杂,我们不再去探究)。需要注意的是: 当调用一个锁对象的wait或notify/notityAll方法时,如果当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁!(这个原因留在评论区讨论)。
5、锁升级分析
由于jdk5(不含)之后对synchroized做了优化,不再是上来就加重量级锁,而是变成: 偏向->轻量级锁->重量级锁 这三种锁级别,如果再加上无锁状态 那一共就是4种,如下:
- 无锁状态-> 偏向锁-> 轻量级锁-> 重量级锁; (但是锁的升级一般是单向的,也就是说只能从低到高升级,不会出现锁的降级(但是高版本似乎也可以降,这一点我们暂时不去关注))
下面我们看下hotspot官方给出的锁升级示意图如下:openjdk的资料点这里;
图中部分英文翻译:(inflate: 膨胀;biased: 偏向;revoke bias: 撤销偏向)
上边这张图片讲的很清楚了(但是必须知道对象头相关的知识才可以看懂这张图,对象头 知识请参见我的另一篇文章:对象组成与Java内存模型JMM分析)
我们对图中做个简单分析:
- 如果开启了偏向锁,那么升级流程大概是:偏向->轻量级->重量级
- 如果没开启偏向,那么升级流程大概是:轻量级->重量级
- 偏向锁记录的是线程id,轻量级锁记录的是指向线程栈的指针,重量级锁指向的是 ObjectMonitor
好了知道大概流程后,我们一级一级来分析一下,到底是怎么膨胀/撤销的
5.1、获取偏向锁
关于monitorenter
指令的处理我们在上边提到过,这里再次简单过一下:
//------------------------------------------------------------------------------------------------------------------------
// Synchronization
//
// The interpreter's synchronization code is factored out so that it can
// be shared by method invocation and synchronized blocks.
//%note synchronization_3
//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
可以看到
- 开启了偏向锁,那么就执行
- ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
- 否则执行:
- ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
那我们就看看 ObjectSynchronizer::fast_enter
到底是干了什么吧?
// -----------------------------------------------------------------------------
// Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
//判断是否开启了偏向锁开关
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
//尝试获取偏向锁
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
//如果是撤销与重偏向 直接返回
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
//走到这里,代表偏向锁未开启,则进入slow_enter获取轻量级锁的流程
slow_enter (obj, lock, THREAD) ;
}
代码似乎很简单,尝试获取偏向锁这个我们就不做过多探究了,但我猜测里边做的事情大概如下:
- 判断: 各种加锁前提条件判断
- 成功: 偏向成功后,将
偏向线程id
和偏向次数
,以及偏向标志
记录到锁对象对象的markword中去。 - 失败: 如果执行偏向操作失败,表示当前存在多个线程竞争锁,当达到全局安全点(safepoint,也即有其他线程参与竞争时),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级,升级完成后被阻塞在安全点的线程继续执行同步代码块。
获取偏向锁的代码演示:
可以看到,在vm配置为开启偏向锁时候(也就是去掉-XX:-UseBiasedLocking参数),就算刚new出来的对象也是标记为偏向锁的,只是线程id(markword前54位全是0),在进入synchroized后,线程id有值,并还是偏向状态,因为此刻只有main线程一个竞争锁
5.2、撤销偏向锁
当到达一个全局安全点时(这里我浅显的理解为没有线程竞争时,实际上安全点的东西很复杂,写一篇万字长文讲清楚都算不错了),这时会根据偏向锁的状态来判断是否需要撤销偏向锁,调用 revoke_at_safepoint方法,这个方法是在 biasedLocking.cpp中定义的,具体实现如下:
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
//断言:必须在全局安全点(这里我浅显的理解为没有线程竞争时,实际上安全点的东西很复杂,写一篇万字长文讲清楚都算不错了)
assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
oop obj = h_obj();
//更新撤销/偏向 计数,并返回偏向锁撤销次数和偏向次数
HeuristicsResult heuristics = update_heuristics(obj, false);
if (heuristics == HR_SINGLE_REVOKE) {
//撤销偏向锁
revoke_bias(obj, false, false, NULL);
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {
//批量撤销
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
//清楚缓存
clean_up_cached_monitor_info();
}
关于断言处提到的安全点这里我们不做过多展开,大概明白这个方法所做的事情大概就是:
- 更新撤销&偏向次数
- 执行撤销/批量撤销
- 由于我们之前讲过对象组成与Java内存模型JMM分析,所以可以肯定的是,撤销偏向时,
会删除偏向线程id
,减掉偏向次数
,将偏向标志从1改为0
。
我们上边说过,当发生线程竞争时,将撤销偏向锁,膨胀为轻量级锁(或者关闭偏向锁时候,发生竞争时直接就进入轻量级锁),下边看看源码:
5.3、获取轻量级锁
在上边的InterpreterRuntime::monitorenter
代码中,我们看到不开启偏向锁的话,是进入到了ObjectSynchronizer::slow_enter
,此中的逻辑就是轻量级锁的获取逻辑。我们看下源码:
// -----------------------------------------------------------------------------
// Interpreter/Compiler Slow Case
// This routine is used to handle interpreter/compiler slow case
// We don't need to use fast path here, because it must have been
// failed in the interpreter/compiler code.
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
//如果是无锁
if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
//copy markword到 当前线程的 线程栈中的Lock Record 的displaced 属性上去
lock->set_displaced_header(mark);
//通过CAS尝试将markword更新为指向BasicLock对象的指针,实际上这一步就是设置Lock Record指针的过程,
//该指针指向 当前执行方法的线程栈
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
}
//如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要抢锁
else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}
#if 0
// The following optimization isn't particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif
// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non-zero to avoid looking like a re-entrant lock,
// and must not look locked either.
lock->set_displaced_header(markOopDesc::unused_mark());
//代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过inflate操作进行膨胀为重量级锁
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
获取轻量级锁 的逻辑大概分为三步,如下:
-
如果当前对象处于无锁状态(即
mark->is_neutral()
成立),则进入步骤(2和3),否则执行步骤(4) -
在
某个(私有的)线程栈
上开辟LockRecord空间,并copy
锁对象对应的markword
到Lock record的_displaced_header
属性中去,下边图解一下:- 在加锁前,hotspot 需要在当前线程的栈帧中开辟锁记录(Lock Record)的空间。
Lock Record 中包含一个 _displaced_header 属性,用于存储锁对象的 Mark Word 的副本。
- 将锁对象的markword复制到锁记录 lock record中,这个复制过来的记录叫做 Displaced Mark Word。具体来讲,是将 mark word 放到锁记录(lock record)的 _displaced_header 属性中(注意:
此时的markword是无锁状态,如果是偏向锁升级到轻量级那么会先撤销偏向,所以displaced_header是无锁的markword! 这个 displaced_header 在轻量级锁释放时候会用到
)。如图:
- 在加锁前,hotspot 需要在当前线程的栈帧中开辟锁记录(Lock Record)的空间。
Lock Record 中包含一个 _displaced_header 属性,用于存储锁对象的 Mark Word 的副本。
-
通过CAS尝试将markword的
ptr_to_lock_record
更新为当前线程栈的地址
(更准确的来说是当前线程栈中的Lock record的地址,为了简单点,我们就叫他为线程栈地址),如果更新成功(同时我们应该清楚的是,抢锁成功后肯定有一个更新最后两位为 00的操作,因为最后两位是00 才代表是轻量级锁),表示竞争到锁,则执行同步代码 -
如果当前markword处于加锁状态,且markword中的
ptr_to_lock_record指针当前线程的栈帧
此时代表重入锁,则执行同步代码, -
否则 说明有多个线程竞争轻量级锁,轻量级锁进行
膨胀升级
变为重量级锁
轻量级锁抢锁举例: 假设某一时刻线程A和B同时
发现锁对象是无锁状态 :
- 此刻 线程A和B都把markword(肯定是同一个markword因为锁对象是同一个)
复制到
各自线程的Lock Record中的 _displaced_header字段,_displaced_header数据保存在线程的栈帧上,是线程私有的 Atomic::cmpxchg_ptr
原子操作保证只有一个线程可以把当前线程栈中的Lock Record地址
复制到markword的ptr_to_lock_record指针上去
,假设此时线程A执行成功(更新markword最后两位为00 ),则A执行同步代码块- 此时线程B执行失败,则退出临界区,通过
ObjectSynchronizer::inflate
方法开始膨胀锁,膨胀成功后变为重量级锁
轻量级锁加锁演示(在这里我们关闭了偏向锁,如果模拟偏向锁到轻量级锁有点不太好模拟,所以我们这里直接关闭偏向锁)
可以看到在关闭偏向锁后,进入同步块的话直接就使用轻量级锁了 可以看到后三位为 0 00 标识此时是轻量级锁状态。
5.4、撤销轻量级锁
与偏向锁不同(偏向锁的撤销是达一个全局安全点时,或者遇到锁竞争,需要升级为轻量级时候
),而轻量级锁的释放,需要执行到monitorexit
指令(也就是说执行完同步块的时候)
//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (elem == NULL || h_obj()->is_unlocked()) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on
// exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
elem->set_obj(NULL);
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
上边的slow_exit是释放锁的主逻辑,而slow_exit中又调了ObjectSynchronizer::fast_exit
所以我们直接看fast_exit这个逻辑如下:
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
// if displaced header is null, the previous enter is recursive enter, no-op
//获取 线程栈帧中的锁记录(LockRecord)中的 displaced_header中的 markword(在加锁时候copy的锁对象中的markword)
markOop dhw = lock->displaced_header();
markOop mark ;
if (dhw == NULL) {
// Recursive stack-lock.
// Diagnostics -- Could be: stack-locked, inflating, inflated.
mark = object->mark() ;
assert (!mark->is_neutral(), "invariant") ;
if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
}
if (mark->has_monitor()) {
ObjectMonitor * m = mark->monitor() ;
assert(((oop)(m->object()))->mark() == mark, "invariant") ;
assert(m->is_entered(THREAD), "invariant") ;
}
return ;
}
//获取锁对象中的对象头
mark = object->mark() ;
// If the object is stack-locked by the current thread, try to
// swing the displaced header from the box back to the mark.
if (mark == (markOop) lock) {
assert (dhw->is_neutral(), "invariant") ;
//通过CAS尝试把当前锁对象Mark Word替换为 dhw的mark word
//(这里的dhw是加锁时候copy的锁对像的 那时候是无锁状态 ,后三位是 0 01 )这一点一定要清楚
//替换成功后,也就代表当前锁对对象的markword的后三位是 0 01 即无锁状态,代表释放锁成功
if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
TEVENT (fast_exit: release stacklock) ;
return;
}
}
//如果CAS失败,说明有其它线程在尝试获取该锁,这时需要将该锁升级为重量级锁并释放
ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}
以上轻量级锁的释放,和那个displaced_header
有很大关系,我们一定要清楚displaced_header是干啥的,不明白的看看轻量级锁加锁时候 displaced_header是放的啥。
以上 轻量级锁的释放 大概是以下几步:
- 取出在
轻量级锁加锁时
保存在当前持有锁的线程栈
中的Lock Record对象上
的displaced_header属性中
的markword数据 然后赋值给 dhw变量 。(我去,一层套一层真的是哈哈哈~)- 值得注意的是 因为在轻量级锁加锁时候,copy markword到线程栈的Lock record的displaced_header中时候,是无锁状态(如果是偏向锁升级到轻量级锁的,会先撤销偏向),也就是当时markword后三位是 0 01,所以在第三步,释放轻量级锁时候,如果dhw替换锁对象 的markword成功,也就意味着释放成功了,这个我们应该要清楚。
- 通过CAS尝试把dhw替换到当前的Mark Word,如果CAS成功,说明成功的释放了锁,否则执行步骤(3);
- 如果CAS失败,说明有其它线程在尝试获取该锁,这时需要将该锁升级为重量级锁,并释放;
5.5、锁膨胀
锁膨胀 我们这里不做源码展开了,简单说下膨胀时候干的几件事情:
判断
当前是否为重量级锁,即Mark Word的锁标识位为 10,如果当前状态为重量级锁,执行步骤(2),否则执行步骤(3);- 获取
指向ObjectMonitor的指针,并返回
,说明膨胀过程已经完成; - 如果当前锁处于膨胀中,说明该锁正在被其它线程执行膨胀操作,则
当前线程就进行自旋等待锁膨胀完成
,这里需要注意一点(虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起);如果其他线程完成锁的膨胀操作,则退出自旋并返回; 如果当前是轻量级锁状
态,即锁标识位为 00,膨胀过程如下:- 通过omAlloc方法,获取一个可用的ObjectMonitor monitor,并重置monitor数据;
- 通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,表示当前锁正在膨胀中,如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成; 如果CAS成功,设置monitor的各个字段:_header、_owner和_object等,并返回;
锁膨胀整体总结:
锁膨胀的过程实际上是获得一个与锁对象对应的ObjectMonitor对象监视器(一个锁对象与一个ObjectMonitor一一对应),而真正抢占锁的逻辑,在 ObjectMonitor::enter方法里面(我们紧接着就要讲到)
5.6、获取重量级锁
接下来我们看下hotspot的代码,来看下主流程:
我们看下里边的主要抢锁逻辑
对应函数为 -> ObjectMonitor::enter:
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
//************: cas操作,将Owner指针 尝试指向给定的线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
// ************: cur == null 代表获取到锁,直接返回
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//************: 获取到锁的是自己 将重入计数器 +1
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//************: 当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀且第一次调用enter方法,那cur是指向Lock Record的指针
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;//设置owner字段为当前线程(之前owner是指向Lock Record的指针)
OwnerIsThread = 1 ;
return ;
}
// We've encountered genuine contention.//遇到了锁资源的竞争
assert (Self->_Stalled == 0, "invariant") ;
Self->_Stalled = intptr_t(this) ;
// Try one round of spinning *before* enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional ...
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
//在调用系统的同步操作之前,先尝试自旋获得锁
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_recursions == 0 , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
//自旋的过程中获得了锁,则直接返回
Self->_Stalled = 0 ;
return ;
}
略 ...
// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
//到这里还没获取到锁,则进行系统调用挂起当前线程(但是这个EnterI也不是直接挂起,
//在插入cxq队列后,他还会尝试获取锁,可以看出是多么的努力~~~)
EnterI (THREAD) ;
...
}
Self->set_current_pending_monitor(NULL);
}
抢锁流程总结: 从上边源码可以得到信息,即当一个线程尝试获得锁时的流程大致如下:
- 如果cas成功,那么就直接返回了。
- 如果 cur == Self 即当前占有锁的线程是自己,那么就会将
_recursions
重入次数加一(多说一句:重入了几次,在释放锁时候就得释放几次,直到次数为0才代表完全释放掉资源)。 - 经过各种尝试后(多次自旋),如果其中有一次成功则返回抢锁成功,如果多次自旋后该锁确实已经被占用,进入到下一步。
- 进入到EnterI 逻辑,其中EnterI 逻辑比较复杂,我们不再贴代码了要不代码占用太多,直接来描述下里边的主要逻辑,如下:
- 进入EnterI后并不是立马将当前线程挂起,而是
先放入cxq队列
,然后再次尝试获取锁
,如果还没获取到锁,才将当前线程挂起
也就是调用park
函数(对于park函数里边的逻辑我们就不展开了,有兴趣可以google下),对应系统调用则是linux
的pthread_cond_wait方法 (作用是挂起线程也可以支持指定时长的挂起)
,我们截图证实一下, hotsport源码点这里
- 进入EnterI后并不是立马将当前线程挂起,而是
代码演示下重量级锁的情况,如下:
可以看到,由于我们在线程池中循环提交了10个任务,也就是说有10个线程在抢锁(竞争相当激烈),所以锁膨胀为重量级 后两位 是 10 代表重量级锁,而后边的 62位是
00000000 00000000 01111111 10011000 01010101 01100001 00111110 000000
,这一串其实就是markword中
的ptr_to_heavyweight_monitor
的值! 。
到此,抢锁逻辑就差不多了,有抢锁就有释放锁,下边我们看下释放锁的逻辑~~
5.7、释放重量级锁
当抢到锁的线程执行完同步块
的逻辑后,会碰到 monitorexit
指令,执行这个指令,线程就会释放锁(值的一提的是如果monitorexit
失败,那么就会在finally中再次monitorexit,确保锁一定释放
,避免死锁,这个小细节在我们的第三节-> synchroized的反汇编中可以观察的到),从而让出资源,供其他线程获取。
注意:(如果是标记为 ACC_SYNCHRONIZED
的同步方法,那么会在方法结束后进行锁释放)
总而言之,不管是同步方法还是同步代码块,都会释放锁,而他们释放锁的hotspot底层对应的都是同一处代码,如下:
void ObjectMonitor::exit(JavaThread* current, bool not_suspended) {
void* cur = owner_raw();
//如果_owner不是当前线程并且当前线程是之前持有轻量级锁的线程 (因为轻量级锁膨胀后还没调用过enter方法),则将_owner指向Lock Record地址。
if (current != cur) {
if (current->is_lock_owned((address)cur)) {
assert(_recursions == 0, "invariant");
set_owner_from_BasicLock(cur, current); // Convert from BasicLock* to Thread*.
_recursions = 0;
}
//略 一大堆
//如果重入次数不等于0 则执行 -- 操作并返回,这里返回后会重新进入exit,也即线程a加了n次锁那么解锁时候就--n次
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
return;
}
for (;;) {
assert(current == owner_raw(), "invariant");
// Drop the lock.
// release semantics: prior loads and stores from within the critical section
// must not float (reorder) past the following store that drops the lock.
// Uses a storeload to separate release_store(owner) from the
// successor check. The try_set_owner() below uses cmpxchg() so
// we get the fence down there.
// 删除锁 从方法名上可以看到 是clear owner ,我猜测最主要的就是通过cas 将owner置为null
release_clear_owner(current);
OrderAccess::storeload();//插入内存屏障 ,保证上边的 release_clear_owner结果对其他正在自旋的线程可见
//(意为:其他正在自旋的线程可以竞争锁了,注意这里抢锁的 可不是在EntryList中的阻塞线程,因为此时还没有唤醒EntryList/cxq
//中的阻塞线程呢得到下边 的 ExitEpilog方法中才是唤醒操作 )
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != nullptr) {
return;
}
// Other threads are blocked trying to acquire the lock.
//到这一步 代表 其他已经阻塞的线程,可以尝试获取锁资源了
//省略 一堆注释,感兴趣可以看看源码中的注释
//由于上边释放了锁,所以在释放锁到此刻如果有线程(通过自旋)抢到的话,当前线程需要抢锁,以便执行下边的唤醒逻辑
if (try_set_owner_from(nullptr, current) != nullptr) {
//没获取到锁(代表在这么一刹那有线程通过自旋获取到锁了) 即 结果 != nullptr 则返回 进行下一次循环
//(即等占用该锁的线程释放后再处理链表 cxq/EntryList 中的等待(blocked)的线程)
return;
}
guarantee(owner_raw() == current, "invariant");
ObjectWaiter* w = nullptr;
//检查EntryList(可以=看出 EntryList比cxq优先级要高) 如果队列中不是空 ,则执行 ExitEpilog逻辑
//(即唤醒等待的线程)
w = _EntryList;
if (w != nullptr) {
ExitEpilog(current, w);
return;
}
// If we find that both _cxq and EntryList are null then just
// re-run the exit protocol from the top.
//如果 cxq和 _EntryList队列都是空,则执行 continue
w = _cxq;
if (w == nullptr) continue;
// Drain _cxq into EntryList - bulk transfer.
// First, detach _cxq.
// The following loop is tantamount to: w = swap(&cxq, nullptr)
//走到这里 代表 _EntryList是空 并且 _cxq不是空,则搬运 cxq元素到 _EntryList队列中去(这也印证了我们说的 _EntryList是排队区,cxq是等待区的说法)。
for (;;) {
//通过cas操作 循环取出 cxq队列的元素 放到w变量中去(后续的唤醒都是从w中取的元素) ,当cxq为空时候跳出循环
assert(w != nullptr, "Invariant");
ObjectWaiter* u = Atomic::cmpxchg(&_cxq, w, (ObjectWaiter*)nullptr);
if (u == w) break;
w = u;
}
assert(w != nullptr, "invariant");
assert(_EntryList == nullptr, "invariant");
// 将 w 赋值给 _EntryList
_EntryList = w;
ObjectWaiter* q = nullptr;
ObjectWaiter* p;
for (p = w; p != nullptr; p = p->_next) {
guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
p->TState = ObjectWaiter::TS_ENTER;
p->_prev = q;
q = p;
}
//(这个注释很重要,我们在ExitEpilog中会详细标注) In 1-0 mode we need: ST EntryList; MEMBAR #storestore; ST _owner = nullptr
// The MEMBAR is satisfied by the release_store() operation in ExitEpilog().
// See if we can abdicate to a spinner instead of waking a thread.
// A primary goal of the implementation is to reduce the
// context-switch rate.
if (_succ != nullptr) continue;
w = _EntryList;//将_EntryList 赋值给 w
//w 不是空 进入 ExitEpilog 执行唤醒操作
if (w != nullptr) {
guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
//唤醒
ExitEpilog(current, w);
return;
}
}
}
//释放锁时候的收尾操作 ps: 比较重要的操作
void ObjectMonitor::ExitEpilog(JavaThread* current, ObjectWaiter* Wakee) {
assert(owner_raw() == current, "invariant");
// 退出时的几个步骤(重要!!!)
// 1. ST _succ = wakee 唤醒线程赋值给 _succ
// 2. membar #loadstore|#storestore; 插入内存屏障 保证其他线程可见
//(这一步插入内存屏障是synchroized可以保证可见性的根本原因,关于内存屏障我们稍后会简单说一下)
// 2. ST _owner = nullptr 将owner指针置为 null
// 3. unpark(wakee) 调用unpark函数,通过操作系统的 pthread_cond_signal 功能(如Linux) 来真正唤醒线程!!!
_succ = Wakee->_thread;
ParkEvent * Trigger = Wakee->_event;
// Hygiene -- once we've set _owner = nullptr we can't safely dereference Wakee again.
// The thread associated with Wakee may have grabbed the lock and "Wakee" may be
// out-of-scope (non-extant).
Wakee = nullptr;
// Drop the lock.
// Uses a fence to separate release_store(owner) from the LD in unpark().
//把owner置null,因为上边释放锁后,在处理等待队列数据时,又加了一次锁,所以这里需要释放锁
release_clear_owner(current);
//这一步就是插入了 #loadstore|#storestore; 内存屏障,即保证当前线程的操作,
//对其他线程可见,也是synchroized实现可见性的根本原因!
OrderAccess::fence();
DTRACE_MONITOR_PROBE(contended__exit, this, object(), current);
//触发unpark操作 唤醒线程
Trigger->unpark();
// Maintain stats and report events to JVMTI
OM_PERFDATA_OP(Parks, inc());
}
以上就是释放锁时候的核心逻辑,可以看到释放锁大概分为以下几步:
- 先做一些判断,然后开始释放锁,并紧跟着插入
storeload
类型的内存屏障 ,保证上边的release_clear_owner(释放锁)
结果对其他正在自旋的抢锁线程可见。 - 如果在此期间没有自旋的线程获取到锁,则重新上锁 ,否则continue 来等待其他线程释放锁
- 上锁成功,开始处理等待队列中的线程
- 检查EntryList如果存在元素,则取出进行唤醒操作
- 如果EntryList是空 并且cxq不是空,则搬运 cxq元素到 w变量
- 将w赋值给EntryList,并标识一些状态
- 将EntryList赋值给w,判断w不是空,进入ExitEpilog方法,进行退出前的
最后/收尾
操作 - ExitEpilog(个人认为比较重要的方法)中有以下几步:
-
- 赋值:
- ST _succ = wakee 唤醒线程赋值给 _succ
- 赋值:
-
- 插入内存屏障: membar #loadstore|#storestore
这一步插入内存屏障是synchroized可以保证可见性的根本原因!!!(可见synchroized和volatile一样,可见性都是通过插入内存屏障来实现的)
- (loadstore:保证在
写主内存
操作前 其他线程一定已经读完
) - (storestore:保证
当前线程写入主内存后
对其他线程可见
)
- 插入内存屏障: membar #loadstore|#storestore
-
- 将owner指针置位null:
- ST _owner = nullptr: 将owner指针 置为 null,释放出锁资源
- 将owner指针置位null:
-
- 通过操作系统的pthread_cond_signal唤醒线程:
- unpark(wakee): 调用unpark函数,通过操作系统的
( 如Linux的pthread_cond_signal )
来真正唤醒线程!!!,进行锁竞争。
- unpark(wakee): 调用unpark函数,通过操作系统的
- 通过操作系统的pthread_cond_signal唤醒线程:
-
内存屏障补充: 上边有关于内存屏障的东西,我们这里做下补充说明。
LoadLoad屏障:
- 对于这样的语句 Load1; LoadLoad; Load2,
- LoadLoad屏障会保证: 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕(说白了: 就是如果插入了LoadLoad内存屏障,就可以保证在Load2读之前,Load1 一定读完了)。
StoreStore屏障:
- 对于这样的语句 Store1; StoreStore; Store2,
- StoreStore屏障会保证: 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障:
- 对于这样的语句Load1; LoadStore; Store2,
- LoadStore屏障会保证: 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
- 对于这样的语句Store1; StoreLoad; Load2,
- StoreLoad屏障会保证: 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
到这里,释放锁的逻辑就结束了,流程图就不画了,我想我已经说清楚主流程了。
总结
- 在第二节:,我们讨论了
多线程的利弊
,并且通过一个小demo引出线程安全问题,然后引出了synchroized
,这节比较简单。 - 在第三节:,我们列出了
synchroized的几种使用方式
,并且简单的观察了其反汇编后的内容
,知道了:- 被synchroized修饰的成员方法/静态方法 会被打上一个 ACC_SYNCHRONIZED 标记
- 被synchroized修饰的代码块,会在执行代码块前插入 monitorenter指令,在退出代码块时候执行monitorexit指令(如果有正常逻辑有异常,hotspot保障还会在finally中执行monitorexit,以避免发生死锁)
- 在第四节:,我们说了真正的互斥资源
ObjectMonitor
,(在假设当前锁升级为重量锁的前提下)
强调了一个锁对象,必然有一个ObjectMonitor与之一一对应,在发生锁竞争的时候,其实是抢锁线程通过cas设置自己的id(实际代码中是一个包装对象,里边含有线程信息)到ObjectMonitor的_owner指针上去
,- 同时我们还图描述了 ObjectMonitor和锁对象的关系 ,把这张图再次晒出来,更直观些:
- 另外,我们描述了ObjectMonitor的流转,包括抢锁成功/失败/入队/唤醒等状态下的示意图,如下:
- 同时我们还图描述了 ObjectMonitor和锁对象的关系 ,把这张图再次晒出来,更直观些:
- 在第五节: (这节偏向源码分析)我们描述了锁升级的过程: 无锁(无锁本文没讲,在我的上一篇有)->偏向->轻量级->重量级 的
锁获取与撤销的源码分析
和代码演示(这个流程图我就偷个懒,从网上找一个吧感觉这个图还算可以,我已经黑眼圈了~~~)。
结语: 到此,本文就结束了,我想这回我对sycnhroized又有了新的认识。另外,还有很多相关的东西没有讲,我想放到后边的文章来说,例如 wait/notify、锁的各种优化(自旋锁、自适应自旋,锁消除,锁粗化)等等,这都放到后边慢慢说吧,由于时间原因,这东西很难一次说完~~~
欢迎评论,留言讨论,一起交流!
巨人的肩膀:
hotspot源码和openjdk的wiki 这两个资料文中已经有粘出来了
竹子大佬的文章: juejin.cn/post/697774… 最后的总结图是参考的这个博客:blog.csdn.net/zhoutaoping… 还有这个文章感觉也不错: www.cnblogs.com/dennyzhangd…