1. 锁的概念
Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等
2. Synchronized的基本使用
synchronized
是Java提供的一个并发控制的关键字。主要用法有两种,分别是同步方法和同步代码块。
synchronized
用法如下:
public class Demo{
//修饰普通方法
public synchronized void functionA(){
//临界区
}
//等价于 synchronized(this)修饰临界区代码块
public void functionA(){
synchronized(this){
//临界区
}
}
//修饰静态方法
public static synchronized void functionB(){
//临界区
}
//等价于 synchronized(A.class)修饰临界区代码块
public static void functionB(){
synchronized(Demo.class){
//临界区
}
}
}
被synchronized
修饰的代码块及方法,不允许被多个线程同时访问。
3. Synchronized实现原理
synchronized
,是Java中用于并发情况下数据同步访问的一个重要关键字。当我们想要保证一个共享资源,在同一时间只会被一个线程访问到的时候,我们可以在代码中使用synchronized
关键字对类或者对象加锁。那么synchronized
关键字到底如何实现上锁的呢?
3.1 Synchronized 修饰代码块
我们将下述用 synchronized
修饰代码块的内容进行反编译:
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
反编译后,我们将得到如下的字节码指令:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object; 获得lock的引用
3: dup
4: astore_1 // lock的引用 -> slot 1
5: monitorenter //将lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // Field counter:I 拿到 counter引用
9: iconst_1 //准备常数 1
10: iadd // +1
11: putstatic #3 // Field counter:I 更新counter的值
14: aload_1 //从之前store的地方拿到 lock的引用
15: monitorexit //将 lock对象MarkWord重置,唤醒 Monitor 中的EntryList(等待队列)
16: goto 24
19: astore_2 //异常情况 e -> slot 2
20: aload_1 // 又再次拿到lock的引用
21: monitorexit //将 lock对象MarkWord重置,唤醒 Monitor 中的EntryList(等待队列)
22: aload_2 //拿到异常
23: athrow //抛出异常
24: return
Exception table: //自动检测异常的代码范围(一般为synchronized修饰的代码块,即临界区)
from to target type
6 16 19 any
19 22 19 any
我们主要关注 main() 方法中被synchronized
修饰的代码块的字节码指令:
编译后的代码中,出现了一些特别的字节码指令,其中monitorenter
字节码指令理解为加锁,monitorexit
字节码指令理解为释放锁。每个对象维护者一个记录着被锁次数的计数器(它将被记录在MarkWord中,后续会谈到)。
字节码中为我们自动添加了两处monitorexit
指令,第二处的monitorexit
指令将在出现异常时被调用,进行锁的释放。(如果我们自己使用其他自定义的锁,也需要在出现异常)
上述指令也反映了:synchronized
修饰代码块的时候,不需要我们主动加锁和解锁的字段,因为它自动在代码块开始和结束的地方替我们补充了加锁和解锁的字节码指令,这也是为什么synchronized
还被称为内置锁。
3.2 Synchronized 修饰方法
我们将下列用synchronized
修饰方法的代码进行反编译:
static int counter = 0;
public synchronized void counterAdd() {
counter++;
}
我们将得到:
public synchronized void counterAdd();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //增加了synchronized标志
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field counter:I 通过#2从常量池Constant Pool中获取到counter引用
3: iconst_1 //准备常数1
4: iadd //+1
5: putstatic #2 // Field counter:I 将+1后的内容更新到#2的引用也就是counter中去
8: return
synchronized
除了修饰在代码块上,还可以修饰在方法上。如果synchronized
修饰在方法上,同步方法的常量池中会有一个ACC_SYNCHRONIZED
标志,当某个线程需要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
标志,如果有,则首先需要获得monitor锁(监视器锁),然后才开始进入临界区,执行方法,方法执行之后再释放monitor锁(监视器锁)。
同样的,如果方法执行过程中遇到异常,并且在方法内部并没有处理该异常,那么再异常被抛到方法外面之前,monitor锁(监视器锁)将会被自动释放。
4. Synchronized的并发性质
4.1 原子性,可见性与有序性
1. 原子性
原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
Java内存模型中,有read
,load
等指令直接保证原子性操作,如果需要更大范围的原子性保证,则可以通过lock
和unlock
来做块的同步,虚拟机提供了字节码指令monitorenter
和monitorexit
来隐式地使用lock
和unlock
这两个操作。反映到Java代码中,就是我们上述提到的synchronized
关键字。
2. 可见性
可见性,即当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
Java内存模型通过:1)在变量修改后,将新值同步回主内存;2)在变量读取之前,从主存刷新变量的值。这两种方式来实现可见性。
Java可以通过volatile
,synchronized
和final
来实现可见性。
这里说final可以用来实现可见性,这篇博文写的不错:
https://zhuanlan.zhihu.com/p/477481115
大概意思是,虚拟机对final关键字的变量的读写加上了内存屏障,前提是final在构造的时候没有发生引用逃逸(它仅保证final的变量,但不保证这个变量所在的类的初始化的构造函数的引用逃逸,多线程场景如果有线程获取到了半初始化的对象,效果就不同了)所以暂时可以认为,final修饰的变量在所在类正确构造之后,是可以实现线程间可见性的。
3. 有序性
有序性,即所有指令的执行顺序是有序的
Java可以通过volatile
关键字来保证有序性。
4.2 Synchronized 的并发性质
-
原子性
由
synchronized
修饰的临界区代码是具有原子性的。可能有多个线程争抢执行同一个临界区的代码,但当一个线程持有该临界区的锁时,即使其他正在争抢的线程被线程调度分到时间片,也无法进入临界区。所有其他线程都要等到当前持有锁的线程执行完临界区代码并退出,才有可能在被调度时,争抢到锁并执行临界区代码。 -
可见性
被
synchronized
修饰的代码,会在开始执行的时候调用字节码指令monitorenter
加锁,执行完成后,会调用字节码指令monitorexit
解锁。为了保证可见性,有一条规则是这样的:“在unlock之前,必须把变量同步回主内存中”,也就是在对一个变量解锁之前,必须先把该变量同步回主存中,这样解锁后,后续线程就可以访问到被修改后的值,从而保证了可见性。
Synchronized 没有有序性
由于处理器优化和指令重排等,CPU有可能对代码进行乱序执行。而需要注意的是,
synchronized
是无法禁止指令重排和处理器优化的。如果要阻止指令重排等来实现指令执行的有序性,则需要使用到volatile
,这不是本章要讨论的内容,不做过多赘述。
5. 对象的内存布局与锁升级
5.1 普通对象和数组独享的内存布局图
对象在实例化之后,是被存放在堆内存中的。这里的对象由三部分组成,如下图:
左边是普通对象,对象头(Object header)包含着MarkWord和类型指针。而数组对象的对象头还多包含了数组长度。
成员变量存储的都是对象的真正的有效数据,也就是各个成员变量属性字段的值,如果拥有父类,还会包含父类的成员变量。
具体对齐填充的规则与JVM有关,在Hotspot中。对象的大小要求向8Byte对齐,当对象长度不足8字节的整数倍时,需要再对象中进行填充操作。
5.1 Markword
在对象头中,MarkWord一共有64个bit,用于存储对象自身的运行时数据,标记对象处于一下状态中的某一种:
在jdk6之前,通过synchronized
关键字加锁的时候,无差别地使用重量级锁,重量级锁会使CPU在用户态和和心态之间频繁切换,有很大的系统消耗。后来随着synchronized
的不断优化,提出了锁升级的概念,在MarkWord中,通过锁的标志位来表示当前对象的锁状态。
- hashcode:代表对象的内存地址值,只有当 hashcode 被使用到的时候,才会填充入该对象的MarkWord内,在此之前值为0.
- 分代年龄:代表对象的分代情况,用于GC的分代垃圾清理。
- 线程指针:指向拥有偏向锁的线程
- epoch: 偏向锁的时间戳,用于偏向锁的重偏向与批量撤销。
Integer包装类为什么比 int 基本类型内存占得更多?
Integer实例作为一个对象,对象头(64bit)加上存的整形数据(32bit)以及其他部分,远远超过 int所需的内存(32 bit)
5.3 锁升级
我们知道了MarkWord可以用于锁升级,那么锁升级的过程是如何的呢?
synchronized
不断优化中,引入了偏向锁,轻量级锁,重量级锁的概念,在不同的情况逐步进行锁膨胀。过程如下图:
5.4 偏向锁
有的对象可能大多使用的场合都只在一个线程中运行,只有少数情况会遇到多线程的竞争,我们不需要直接设置竞争机制,或者直接上消耗资源的锁。我们可以通过偏向锁这样,只在MarkWord上加一个线程ID的标记的低消耗的方式,来标志对象正在被使用。当遇到争抢的时候,再依情况进行锁升级。例如StringBuffer
中很多方法都被synchronized
修饰,但是它的使用场景经常只在一个线程中执行。
5.4.1 虚拟机开启偏向锁
Hotspot JVM中,偏向锁默认是开启的,而且是从程序开始计时起,延迟4s开启。
JVM参数可以开启或者关闭偏向锁:
//开启:
-XX:+UseBiasedLocking
//关闭:
-XX:-UseBiasedLocking
在IDEA中开启偏向锁
默认偏向锁在程序启动4s后开启。在JVM还没开启偏向锁的时候,如果用synchronized
修饰,将会直接升级为轻量级锁。
修改偏向锁延迟时间的JVM操作为:
-XX:BiasedLockingStartupDelay=0
开启偏向锁后,Java对象被创建后将会是匿名偏向态(可偏向态),未开启偏向锁,Java对象被创建后将会是普通对象(无锁态)。
5.4.2 普通对象与匿名偏向与偏向锁态
普通对象的 MarkWord 格式为:
没有开启偏向锁时,当对象首次创建的时候,hashcode、分代年龄均以 0 作为初始值。其中 hashcode 只有被使用的时候,才会被赋予值,例如该对象调用了 hashcode()
方法。
偏向锁态的MarkWord格式为:
开启偏向锁后,对象实例化后,还没有线程对它上锁(还没有线程调用的方法对该实例使用synchronized
修饰),线程ID 为初始值 0。也就是还没偏向任何一个线程。这个状态被称为 匿名偏向。
此后,第一个对该对象上锁(调用的方法中对该实例使用synchronized
修饰)的线程,将会把 线程ID 记录到 MarkWord中。此时MarkWord的偏向锁位被置为 1
,且锁标志位为 01
。 此时这个对象就可以理解为偏向该线程,为偏向锁态。
5.4.3 偏向锁的加锁过程
当 JVM 的偏向锁开启后,就可以对实例进行偏向锁的加锁。
不需要特别的加锁语法,我们只需要使用 synchronized
修饰,虚拟机就会根据是否开启偏向锁等条件,自动帮助你实现加锁。如:
public static void test(){
synchronized(this){
//do something...
}
}
假设 Thread-0 想要对 Object 对象上锁:
1. 加锁成功情况
a. 首次加锁
-
判断锁对象是否是匿名偏向状态。
-
如果是,Thread - 0 使用 CAS (Compare and Swap)尝试将 线程ID 赋值给 Object的MarkWord。由于Object的 线程ID 还未被使用(对象创建过后,若为匿名偏向状态,线程ID 字段的初始值为0),赋值成功。
-
如果线程ID已被占用,而且不是不是Thread -0 的线程ID,说明该锁已经偏向别的线程,加锁失败。
-
此时 Thread-0 线程首次给 Object 对象加了锁,Object 对象的MarkWord中的 线程ID 就会指向 Thread - 0,
需要注意的是这里所说的 线程ID 是操作系统赋予的唯一标识,并不是 Java 中 Thread.getId()
获取到的ID。
b. 再次加锁(锁重入)
synchronized
修饰的是可重入锁,如果在同一线程中,偏向锁对象是允许锁的重入的。不过偏向锁的重入很简单,只需要判断锁对象的 线程ID 是否是当前线程即可。
Thread - 0 对 Object 首次加锁成功后,后续只需要判断 线程ID 是否还是自己即可,不需要重新 CAS 将 线程ID 更新到 Object 的 MarkWord 中。
偏向锁的重入没有计数器,只要线程ID还是自己,就认为可重入,即认为可以进入临界区。(同一线程中为顺序执行,不会有线程冲突)
2. 加锁失败的情况
假设 Object锁对象 已经被 Thread - 0 占用,此时线程 Thread -1 来竞争锁对象,流程将如下:
- 判断是否为偏向锁;
- 判断偏向锁指向的 Thread-0 是否还存在;
- 如果线程 Thread - 0 还存活,则暂停 Thread -0 ;
- 将锁标志位设置为 00 (变为轻量级锁态);【撤销偏向锁,并锁膨胀】
- 从线程1的栈帧中的 Lock Record 与 Object对象的MarkWord做更新(细节请见轻量级锁的加锁规则);
- 继续执行 Thread -0 的代码;
- 线程 Thread -1自旋来争取锁对象;
如果在第二步发现 Thread-0已经不存在了,将会尝试进行Object偏向锁的重偏向。如果无法重偏向,将会进行锁升级,上述流程将会变为如下:
- 判断是否为偏向锁;
- 判断偏向锁指向的 Thread-0 是否还存在;
- 线程 Thread -0 不存在,且暂时无法进行重偏向;
- 将锁标志位设置为 00 (变为轻量级锁态);【撤销偏向锁,并锁膨胀】
- Thread -1 对 Object 进行轻量级锁的加锁行为。(见轻量级锁的加锁)
如果成功重偏向,将与下述的重偏向一致:
5.4.4 偏向锁的解锁(重偏向)
偏向锁没有实际意义上的解锁,离开
synchronized
代码块后,不会对偏向锁对象的 MarkWord 做修改,即偏向锁态的 MarkWord 中的 线程ID 仍然指向已经过时的线程。偏向锁作为最“轻”的锁,这样设计的好处是:
下次同一线程再次上锁,只需要判断自己是否曾经持有过锁,而不用再通过 CAS 去替换 MarkWord 中的 线程ID 。如此减少了撤销偏向锁的资源消耗,也减少了同一线程重入时候的CAS消耗。
如果对象虽然被多个线程访问,但是没有竞争,此时偏向原线程(如上述例子的Thread -0) 的对象就有了重偏向其他线程(如上述例子的 Thread-1) 的机会。
重偏向条件:JVM会记录撤销偏向锁的次数,一旦撤销次数超过 20,后续该类中的对象都可以重新偏向加锁线程(即将 线程ID 指向新的线程)。后续线程对重偏向的对象加锁,仍为偏向锁。
重偏向的优点:升级为轻量级锁将会增加CPU的负担,如果没有竞争,可以通过重新偏向来进行锁复用的优化。
偏向锁的撤销:
- 锁对象使用到 hashcode,将会禁用偏向锁,将锁标志位变为 001,变为无锁态(不可偏向状态),并将hashcode赋值。
- 发生竞争,偏向锁被升级为轻量级锁。
- 调用wait()/notify()等方法,直接将偏向锁升级为重量级锁。
5.4.5 偏向锁的解锁(批量撤销)
正如重偏向所介绍的,JVM会记录撤销偏向锁的记录,与重偏向不同的是,撤销次数超过 40的时候,整个类的所有对象均变为不可偏向(锁标志位都变为无锁态001),包括该类中未来新建的对象也是不可偏向。
无锁态的对象认为偏向锁未开启,未来只会升级为轻量级锁,或者重量级锁,而不会再变为偏向锁。
开启偏向功能的对象,在初始化之后,MarkWord 锁标志位是 101,是可偏向状态
没开启偏向功能的对象,在初始化之后,MarkWord 锁标志位是 001。是不可偏向/Normal状态。
5.5 轻量级锁
5.5.1 偏向锁->轻量级锁
当只有一个线程访问偏向锁的时候,仅仅是把线程ID记录到MarkWord中,没有额外的系统消耗。
但是,当有新的线程也需要对该对象上锁,发现该对象已经被别的线程持有偏向锁(观察到MarkWord中有别的线程ID),此时的竞争升级。
系统将会撤销偏向锁,并将锁升级为轻量级锁(自旋锁)。
设置轻量级锁,首先需要在当前线程的栈帧中创建一个Lock Record,通过使用CAS 尝试将 Lock Record与 Object的 MarkWord 互换,从而进行锁的争抢。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈(VM Stack)的栈元素。
5.5.2 轻量级锁的加锁过程
1. 加锁成功的情况
a. 从无锁态加锁为轻量级锁
假设 Thread - 0 要对 Object 上锁,且为轻量级锁的加锁
-
【判断锁标志位】判断 MarkWord 是否为无锁态(锁标志位为 001 )。
-
【创建Lock Record】Thread-0在自己的线程栈的栈帧中创建一个 Lock Record;
-
【配置Lock Record】新建的 Lock Record 中的 记录内容为 当前 Lock Record 的地址,锁对象的引用(Object reference) 为锁对象的内存地址;
-
【CAS交换LR与MarkWord】通过 CAS 尝试进行将 Lock Record 的内容与锁对象的 MarkWord 做交换。过程如下图:
-
交换成功即上锁成功,上锁后的情况如下图:
b. 从偏向锁升级为轻量级锁(锁对象不在重偏向状态)
假设 Thread -1要对 Object上锁,Object当前状态为偏向锁态( MarkWord 锁标志位为 101)
- 【判断锁标志位】将 MarkWord 中的锁标志位更改为 00;
- 【找到原线程】如果持有偏向锁的线程还在运行;
- 【添加Lock Record】在 自己 和 持有锁的线程 的栈帧中添加 Lock Record;
- 【CAS交换 LR 与 MarkWord】 持有锁的线程 通过CAS 尝试将 Lock Record 与 锁对象的 MarkWord做交换;
- 【CAS交换 LR 与 MarkWord】如果没有 持有锁的线程 ,则自己通过CAS 尝试将 Lock Record 与 锁对象的 MarkWord做交换;
- 交换成功即上锁成功。
如果锁对象在重偏向状态,第二个线程将会对偏向锁态的对象进行重偏向到自己线程ID的行为,而不会升级为轻量级锁
c. 锁的重入
假设Thread-0已经持有Object的轻量级锁,此时在本线程内又要对其上锁,即锁重入,流程如下:
- 【判断锁标志位】判断 MarkWord 的锁标志位,
- 【判断本次是否重入】如果是轻量级锁态,通过 MarkWord 的
ptr_to_lock_record
来判断是否本线程持有锁; - 【非重入则锁膨胀】如果不是锁重入的情况,说明竞争冲突,膨胀为重量级锁;(见重量级锁的升级/加锁)
- 【是重入则加空 Lock Record】如果是锁重入,将在自己的栈帧中加一个 Lock Record,与初次上锁不同的是,记录内容为 空(null),锁对象的引用仍然也指向 Object锁对象。
需要注意的是,轻量级锁的重入也无需计数器,在解锁过程中,遇到 LockRecord 的记录为空,说明是重入的锁,遇到记录不为空,才算真正解锁。
锁重入的流程图如下:
2. 加锁失败的情况
如果遇到锁对象的 MarkWord 的锁标志位为00,说明该锁对象正在被一个线程持有,且为轻量级锁态。
如果持有锁的线程不是自己,则加锁失败,进入锁升级流程(见重量级锁)。示意图如下:
当自旋到一定轮数,或者自旋线程数到达一定数量,该锁对象将会升级为重量级锁态,具体见后面探讨的重量级锁。
5.5.3 轻量级锁的解锁过程
退出栈帧的时候,如果Lock Record的记录为null,说明是重入锁,仅仅是弹出栈帧。
如果Lock Record的记录有值,说明本次解锁是真正解锁(接触的不是重入的锁)。
解锁的过程为:
-
【判断锁标志位】如果 MarkWord 的锁标志位为 00(如果锁标志位为 10,说明在持有过程中,该锁对象升级为了重量级锁)
-
【如果锁对象是重量级锁】如果 MarkWord的锁标志位为 10,根据 MarkWord 的
ptr_to_monitor
来找到 ObjectMonitor ,并将owner置为null,同时唤醒 EntryList,由操作系统调度唤醒等待队列中的线程; -
【如果锁对象仍然是轻量级锁】如果 MarkWord 的锁标志位为 00 ,通过CAS尝试 将Lock Record的 record 的值与Object锁对象的 MarkWord 交换;
-
【解锁结果】交换完成后锁对象的 MarkWord 的锁标志位变为 001(上锁前为无锁态),或者101(上锁前为偏向锁态);
-
【退出栈帧,并删除 Lock Record】线程栈中删除对应的Lock Record。
问:有偏向锁,为什么还需要轻量级锁?
答:偏向锁在撤销的时候会消耗系统资源,在争抢激烈的时候,效率没有轻量级锁高。
问:为什么JVM默认一开始不打开偏向锁?
答:JVM启动过程中会有很多线程争抢,所以默认启动时不打开偏向锁,而是默认等待4s过后才打开偏向锁。
5.6 重量级锁
5.6.1 轻量级锁 -> 重量级锁
有的教程说:当发生重度竞争时,即1)有线程超过10次自旋, 2)自旋线程数超过了CPU核数的一般。JVM将会把锁升级为重量级锁。
但我们看到JDK8的源码后,并不是这样,它是直接膨胀为重量级锁,但并不是直接阻塞等待,而是会先进行一会儿 自适应自旋。
重量级锁需要内核态的参与, JVM 向操作系统申请ObjectMonitor
,这是一个C++对象,需要内核态才能访问。锁对象的 MarkWord 指向 ObjectMonitor 的地址。
ObjectMonitor 的主要结构如下图:
Owner: 指向持有该 ObjectMonitor 的线程。
EntryList: 等待队列,队列中的线程都为 BLOCKED 状态(Java的六种线程状态之一),等待锁。
WaitSet: 存放处于 WAITING 状态(Java的六种线程状态之一)的线程
Recursions: 持有该 ObjectMonitor 的线程的重入次数。
重量级锁拥有等待队列,没有争抢到锁的线程经过自适应自旋后,进入等待队列,变为 BLOCKED 状态,等待被唤醒。再次启动去争取锁的过程将比较耗时。
5.6.2 重量级锁的加锁过程
当线程获取轻量级锁失败,或者持有偏向锁/轻量级锁的线程运行在临界区,遇到obj.wait()或者obj.notify()方法,都进入重量级锁升级状态。重量级加锁流程如下:
- 【申请ObjectMonitor】让操作系统为 Obj 申请 ObjectMonitor 锁;
- 【配置ObjectMonitor的owner】如果 Obj 原先是轻量级锁,让 ObjectMonitor 的 owner 指向持有轻量级锁的线程(通过轻量级锁对象 MarkWord 的 ptr_to_lock_record);
- 【配置ObjectMonitor的owner】如果 Obj 原先是偏向锁,让 ObjectMonitor 的owner 指向持有偏向锁的线程(通过偏向锁对象 MarkWord 的 线程ID);
- 【配置ObjectMonitor的owner】如果 Obj 原先是无锁态,让 ObjectMonitor 的 owner 标空;
- 【更新Obj锁标志位】并将MarkWord锁标志位改为 10 (重量级锁态);
- 【将Obj与Monitor关联起来】让 Obj 的MarkWord指向 ObjectMonitor 地址;
- 【线程自适应自旋】线程进行自适应自旋,如果在最大自旋次数内争抢成功,使得 ObjectMonitor 的owner指向自己,则加锁成功。
- 【进入等待队列】如果在最大自选次数内没有争抢成功,则进入 ObjectMonitor 的 EntryList等待队列中等待,并进入 BLOCKED 状态(Java线程的六个状态之一)。
第二步,从轻量级锁升级为重量级的图示如下:
第三部,从偏向锁升级为重量级锁的图示如下:
自适应自旋:
根据情况,增加或减少最大次数的自旋。
JDK6之后使用了自适应自旋,如果线程通过自旋获得重量级锁,则下一次再有线程进行自适应自旋来争取该 ObjectMonitor ,可以提高几次的最大自旋次数。
如果线程在最大次数自旋内没有获得重量级锁(没有争抢到 ObjectMonitor 的owner),则下次再有线程通过自适应自旋来争取该 ObjectMonitor,则减少最大自旋,甚至不进行自旋,直接进入 EntryList 而进入 BLOCKED 状态。
5.6.3 重量级锁的解锁过程
- 将 recursion 重入次数减 1。
- 如果recursion为0,说明本次是真正的解锁,将owner置空,并根据操作系统来唤醒EntryList中的线程。
唤醒:只是将线程变为 就绪态,可以被 CPU 分配时间片,并不代表立即运行。
问:有轻量级锁,为什么还需要重量级锁?
答:线程自旋占CPU资源,重度竞争的时候,自旋的时间很长,或者自旋的线程很多,CPU花了更多时间在线程切换上。重量级锁通过内核态参与,将没有争抢到锁的线程阻塞,排队等待,减轻了系统资源的消耗。
5.3.4 代码中如何选择锁状态的?
我们前面说到,Java通过synchronized
关键字对类或者对象上锁,在字节码指令中会有monitorenter
和monitorexit
,在底层代码中,monitorenter
首先判断偏向锁是否开启,尝试加偏向锁,在 sharedRuntime.cpp中逻辑为:
Handle h_obj(THREAD, obj);
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK);//上偏向锁
} else {
ObjectSynchronizer::slow_enter(h_obj, lock, CHECK);//上轻量级锁
}
在 fast_enter()
中,如果偏向锁上锁失败,将会转为加轻量级锁,进入 synchronizer.cpp#fast_enter我们看到:
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(obj, lock, THREAD);//最后,进入轻量级锁的获取
}
申请轻量级锁,进入synchronizer.cpp#slow_enter我们看到:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
//...
// 如果是无锁状态
if (mark->is_neutral()) {
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} 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;
}
创建/获取 ObjectMonitor对象的过程就是锁的膨胀过程,看到 synchronizer.cpp#inflate()方法:
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
...
//死循环,直到获取到ObjectMonitor对象为止
for (;;) {
//取出Mark Word
const markOop mark = object->mark() ;
//如果是重量级锁
if (mark->has_monitor()) {
//是重量级锁,说明之前已经有现成的ObjectMonitor,直接使用
ObjectMonitor * inf = mark->monitor() ;
return inf ;
}
//正在膨胀的时候
if (mark == markOopDesc::INFLATING()) {
//继续循环,需要等待膨胀完成
continue ;
}
//如果当前是轻量级锁
if (mark->has_locker()) {
//先将ObjectMonitor对象的owner指向自己
ObjectMonitor * m = omAlloc (Self) ;
//初始化一些参数
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
//尝试将Mark Word的锁标志位更改为 10
//可能会有多线程走到这,因此利用CAS的原子性来保证线程安全地更改锁的标志位为 10(重量级锁态)
//(如果当前线程是轻量级锁,说明有线程正在持有该锁,尝试CAS修改锁为膨胀状态)
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
//判断是否修改成功,如果不成功,继续循环。
if (cmp != mark) {
//修改失败,继续循环
omRelease (Self, m, true) ;
continue ;
}
//若是修改成功,则取出之前轻量级锁存储的Mark Word
markOop dmw = mark->displaced_mark_helper() ;
//将Mark Word 搬到ObjectMonitor的_header字段里
m->set_header(dmw) ;
//之前默认owner是自己,实际需要_owner指向Lock Record,也就是设置锁的持有者是Lock Record,更新owner
m->set_owner(mark->locker());
//指向对象头
m->set_object(object);
//将Mark Word 指向ObjectMonitor
object->release_set_mark(markOopDesc::encode(m));
...
//成功,则返回ObjectMonitor 对象
return m ;
}
//无锁状态
ObjectMonitor * m = omAlloc (Self) ;
//初始化一些参数
m->Recycle();
//直接记录mark
m->set_header(mark);
//无所状态,_owner为空
m->set_owner(NULL);
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
//将Mark Word修改为指向ObjectMonitor的指针
//使用CAS来锁状态为重量级锁。
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
...
//失败,则重新尝试
continue ;
}
...
//成功,则返回ObjectMonitor 对象
return m ;
}
}