Java中的锁
所谓锁是用来控制多个线程访问共享资源的方式,所以锁在Java中并不仅仅指代的就是Java中的对象,例如自旋锁用的就是指令的方式。通常的,一个锁能够防止多个线程访问共享资源(也称为临界区)。
在JDK5之前,Java中的锁是通过关键字synchronized来实现的,而在JDK5之后Lock接口的出现使得锁的使用更加的灵活。同时在JDK6开始对synchronized关键字进行了锁升级的优化,使其能够适用于更多的场景,不再是严格意义上的重量级锁。
synchronized关键字
同步的,synchronized关键字可以隐性的将一个java对象设置为锁,在《Java虚拟机规范》中并没有对synchronized做特定的约束,synchronized的实现依照各虚拟机产商而不同。在Hotspot(常说的Java虚拟机基本就是指这个)中是在对象的头部信息中使用mark word域(32位或者64位)来表示对象的锁信息。在JDK6的优化之后,synchronized关键字被引入了一个锁升级
的概念,其升级过程如下:
偏向锁 ---> 轻量级锁 ---> 重量级锁
锁的升级和锁对象的头部信息的mark word字段息息相关,对象头结构及各种状态下的mark word域的结构如下图:
参考:《Java并发编程的艺术》P12
其中mark word可以用来储存对象的hashcode、锁信息、垃圾回收时的分代年龄等信息。Class Metadata Address保存对象的类型(class类)数据的指针。32位的Array Length用在数组对象中保存数组对象的长度。
不同状态下mark word的内容:
参考:《黑马程序员-并发编程》
因此一个对象有如下四种锁状态:“无锁状态”、“偏向锁状态”、“轻量级锁状态”、“重量级锁状态”。其中:
- baised_lock表示是否是偏向锁,0表示不是偏向锁,1表示是偏向锁;
- age表示垃圾回收时的分代年龄;
- epoch用在批量重偏向中;
- threadID表示获取偏向锁的线程ID;
- ptr_to_lock_record指向栈帧中的lock record记录;
- ptr_to_monitor指向monitor对象。
&nsbp;
偏向锁状态
偏向锁指的是当锁不存在多个线程竞争,并且经常由一个线程获取锁对象时,为了让线程获取锁的代价更低,在锁对象的mark word中保存了当前获取锁对象线程的线程ID,下次该线程要获取锁的时候可以不使用CAS的方式获取锁,而是直接通过比较线程ID来再次获取锁对象。
例如:
判断线程ID一样之后就会直接获取锁,而不会采用cas的方式来自旋获取锁。同时采用这种方式偏向锁即是可重入的锁。
一个对象在刚被创建的时候处于无锁状态(mark word后三位为:010,其余全为0),但是几秒之后会变成偏向锁状态(mark word后三位为:110,其余全为0),可以使用虚拟机参数:“-XX:BiasedLockingStartupDelay=0”来关闭这种延迟的特性,同时如果想要关闭偏向锁的话可以使用虚拟机参数:“-XX:-UseBiasedLocking”来进行关闭。
例如:
输出的mark word的主要内容如下:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
说明:value字段的内容为16进制,最后的5用二进制位表示为:0101,即为偏向锁状态。(注意这里在测试的时候并没有添加关闭偏向锁延迟加载的虚拟机参数,但是可能由于程序启动太慢了导致第一个显示的也是偏向锁状态?)
添加了虚拟机参数“-XX:-UseBiasedLocking”关闭偏向锁后的输出:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000004bd2bff420 (thin lock: 0x0000004bd2bff420)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
说明:关闭偏向锁之后在第一次调用synchronized时候直接变成了轻量级锁状态。
备注:jol依赖引入如下,注意scope属性不要provider!
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
轻量级锁
轻量级锁状态发生在当有多个线程访问同步代码块,但是访问时间是错开的,彼此之间没有竞争的时候。(注意偏向锁是发生在同一个线程反复访问同步代码块的时候。)
轻量级锁状态的记录不再跟偏向锁一样在对象的mark word域中记录thread id,而是使用了线程栈中的栈帧的锁记录结构来记录轻量级锁状态。
-
加锁过程:让锁记录中的对象引用指向对象地址,同时采用CAS的方式将锁记录的地址替换到对象的mark word域中,对象的hashcode和分代年龄等信息则放到锁记录中进行暂存。CAS操作有如下两种情况:
- CAS成功则当前线程获取锁,对象头的mark down信息存储
锁记录地址和最后面的锁状态改为00
。
- CAS成功则当前线程获取锁,对象头的mark down信息存储
- 如果失败,有两种情况,一种是确实是当前线程获取了锁对象,这个时候就发生了锁的重入,只需要多添加一条Lock Record作为锁的重入计数即可。
另外一种是别的线程获取了锁对象正在访问同步代码块,这个时候就会发生锁膨胀,将轻量级锁膨胀为为重量级锁。
- 解锁过程:当退出同步代码块发现有取值为null的锁记录,则证明有重入现象,只要去掉即可。如果取值不为null,则用CAS操作将对象的mark word内容重置。该操作有两种情况:
- 成功:则解锁成功。
- 失败:说明轻量级锁已经膨胀成了重量级锁,因为这个时候对象头已经不再是指向锁记录了,而是保存了重量级锁的monitor信息,进入重量级锁的解锁流程。
重量级锁
Monitor对象
重量级锁和一个Monitor对象有关。每个Java对象都可以关联一个Monitor对象,如果使用synchronized关键字给对象加上重量级锁的时候,锁对象的mark word域就会指向Monitor对象。
Monitor对象的结构如下:
- Monitor刚创建的时候Owner为null。
- 当Thread-0执行synchronized(object)时,Monitor就会将Owner设置为Thread-0,Monitor只能有一个Owner。
- 在Thread-0上锁的过程中,如果有其他线程Thread-1、Thread-2等线程来竞争锁,就会进入EntryList Block中。
- 在Thread-0中执行完同步代码块后,会唤醒EntryList中的等待线程来竞争锁,竞争的时候是非公平的。
- WaitSet的内容与wait-notify有关。
注意:
- synchronized必须是进入同一个对象的monitor才有上述效果。
用下面代码为例查看monitor起的作用
static final Object obj = new Object();
static int count = 0;
public static void main(String[] args) {
synchronized (obj) {
count++;
}
}
对应的字节码:
Code:
stack=2, locals=3, args_size=1
0: getstatic #7 // Field obj:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 插入monitorenter指令
6: getstatic #13 // Field count:I
9: iconst_1
10: iadd
11: putstatic #13 // Field count:I
14: aload_1
15: monitorexit // 插入monitorexit指令
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
当synchronized处于重量级锁的状态的时候,JVM会在进入同步代码块之前插入monitorenter指令,保证线程获取锁;在退出代码块之前插入monitorexit指令,保证锁被释放。
当synchronized处于重量级锁的状态的时候,线程的竞争不会自旋,不会消耗CPU,交由操作系统处理;此时竞争的线程会被阻塞。
【参考】
- 《Java并发编程的艺术》
- 《黑马B站并发教程》