文章目录
一、Synchronized的使用
Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized
关键字的使用方式主要有下面 3 种
-
修饰实例方法
-
修饰静态方法
-
修饰代码块
1、修饰实例方法
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {
//业务代码
}
2、修饰静态方法
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
静态
synchronized
方法和非静态synchronized
方法之间的调用不互斥
- 如果一个线程 A 调用一个实例对象的非静态
synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态synchronized
方法,是允许的,不会发生互斥现象,因为访问静态synchronized
方法占用的锁是当前类的锁,而访问非静态synchronized
方法占用的锁是当前实例对象锁。
3、修饰代码块
对括号里指定的对象/类加锁:
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
4、总结:
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁;synchronized
关键字加到实例方法上是给对象实例上锁;- 尽量不要使用
synchronized(String a)
,因为 JVM 中,字符串常量池具有缓存功能,多个线程使用相同的字符串值,实际使用的是同一个对象
二、Monitor
Java对象由三部分组成
- 对象头
- 对象体:对象体里放的是非静态的属性,也包括父类的所有非静态属性(private修饰的也在这里,不区分可见性修饰符),基本类型的属性存放的是具体的值,引用类型及数组类型存放的是引用指针。
- 对齐填充
1、Java对象头
1.1 32 位虚拟机的对象头
普通对象
Mark Word
:存储对象自身的运行时数据,hashCode、gc年龄以及锁信息等
Klass Word
:指向Class对象
数组对象
相对于普通对象多了记录数组长度
所以对于一个int类型整数来说,它占用4字节,而一个
Integer
对象,在32位虚拟机中包含了8字节对象头,4字节数据,一共12字节,加上内存对齐,就是16字节
1.2 64位虚拟机的对象头
- Markword:存储对象自身运行时数据如hashcode、gc分代年龄及锁信息等,64位系统总共占用8个字节。
- 类型指针:对象指向类元数据地址的指针,jdk8默认开启指针压缩,64位系统占4个字节
- 数组长度:若对象不是数组,则没有该部分,不分配空间大小,若是数组,则为4个字节长度
2、Mark Word 结构
32位虚拟机
64位虚拟机
![](https://img-blog.csdnimg.cn/img_convert/61bceac1514c9b29e77880dc716bf3ca.png)
- 对象的hashCode占31位,重写类的hashCode方法返回int类型,只有在无锁情况下,在有调用的情况下会计算该值并写到对象头中,其他情况该值是空的。
- 分代年龄占4位,最大值也就是15,在GC中,当survivor区中对象复制一次,年龄加1,默认是到15之后会移动到老年代。
- 是否偏向锁占1位,无锁和偏向锁的最后两位都是01,使用这一位来标识区分是无锁还是偏向锁。
- 锁标志位占2位,锁状态标记位,同
是否偏向锁标志位
标识对象处于什么锁状态。 - 偏向线程ID占54位,只有偏向锁状态才有,这个ID是操作系统层面的线程唯一id,跟java中的线程id是不一致的
3、Moniter
Moniter称为监视器或者管程,是操作系统提供的对象
每个Java对象都可以关联一个Moniter对象,如果使用synchronized
给对象上锁(重量级),该对象的Mark Word中就被设置指向Moniter对象的指针
Moniter结构
- 刚开始Moniter中Owner为null
- 当Thread-2执行
synchronized(obj)
后,就会将Moniter的所有者Owner置位Thread-2,Moniter只能有一个Ownerobj
对象的MarkWord
中最初保存的是对象的hashcode、gc年龄等信息,同时锁标志位为01,表示无锁。当获取锁后,会将这些信息保存在Moniter对象中,然后MarkWord
存储的就是指向Moniter的指针,锁标志位为10(重量级锁)
- 在Thread-2上锁的过程中,如果Thread-1、Thread-3也来执行
synchronized(obj)
,就会进入EntryList
,处于BLOCKED
状态 - Thread-2执行完同步代码块的内容后,唤醒
EntryList
中等待的线程来竞争锁,竞争是非公平的
4、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 // <- lock引用 (synchronized开始)
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
0:拿到lock的引用
4:将lock的引用存储到 slot1 中
5:将lock对象的 MarkWord 置为 Monitor 指针,原本存储的信息就存储到 Monitor 中
6-11:执行
counter++
操作14:从 slot1 中获取lock对象的引用
15:将 lock 对象的 MarkWord 重置,原本MarkWord 存储的是hashcode、gc年龄等信息,当 lock 获取锁后,将MarkWord 置位 Monitor 指针。重置就是将这些信息重新写到 MarkWord 中,同时唤醒 EntryList
16:
goto 24
执行24行,退出在Exception table中设置了监控异常的行数,如果6-16行有异常,就去执行19行
19:将异常信息 e 存储到slot2中
20:从 slot1 中获取lock对象的引用
21:将 lock对象 MarkWord 重置, 唤醒 EntryList
22:从 slot2 中获取异常信息
23:打印异常信息
注意
- 通过异常 try-catch 机制,确保一定会被解锁
- 方法级别的 synchronized 不会在字节码指令中有所体现
5、轻量级锁
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
如下,method1
和method2
方法都对obj
对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
1、创建 锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
锁记录有两个属性
- 锁记录地址,同时用
00
标记表示轻量级锁 Object referenct
指向锁的对象
锁的对象obj
中有对象头、对象体,对象头中Mark Word
存储的是hashcode、gc年龄等信息
![](https://img-blog.csdnimg.cn/img_convert/a448480e446f00a562ee75b8b980823a.png)
2、让锁记录中 Object reference
指向锁对象,并尝试用 cas 替换 Object
的 Mark Word
,将 Mark Word
的值存入锁记录,然后将锁记录的信息存到Object
的Mark Word
中
![](https://img-blog.csdnimg.cn/img_convert/f42d1af9f39881e7a355d1301cff1139.png)
3、如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00
,表示由该线程给对象加锁
![](https://img-blog.csdnimg.cn/img_convert/19d9e2f464bfc1bb5cccd0561f78f694.png)
4、如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了
synchronized
锁重入,那么再添加一条Lock Record
作为重入的计数- 此时锁记录中
Object reference
指向Object
,但是由于Object
的Mark World
位置已经是00
轻量级锁状态,因此这条锁记录存储为null
- 此时锁记录中
![](https://img-blog.csdnimg.cn/img_convert/01c7fca73fa6513106d22f2e8e3776e9.png)
5、当退出 synchronized
代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
![](https://img-blog.csdnimg.cn/img_convert/1cf55de111a52d431eeb4e2855a9fd6b.png)
6、当退出 synchronized
代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word
的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
6、锁膨胀
锁膨胀:轻量级锁升级为重量级锁
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 为 Object 对象申请
Monitor
锁,让Object
的MARK WORLD
指向Moniter
锁地址 - 在堆区创建一个锁记录【
Lock Record
】对象,该对象包含了持有该锁的线程信息,然后Object
的MARK WORLD
也会记录这个对象的地址 - 然后 Thread-1进入
Monitor
的EntryList
,处于BLOCKED
状态
3、当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word
的值恢复给对象头,会失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
7、自旋优化
重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)
自旋重试成功的情况
自旋重试失败的情况
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
8、偏向锁
-
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
-
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
这里的线程id是操作系统赋予的id 和 Thread的id是不同的
例
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
没有开启偏向锁,会使用轻量级锁 ,每次重入都会执行CAS操作 开启偏向锁,每次锁重入仅判断当前ThreadID
是否是自己
对象头格式
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,
markword
值为0x05
即最后 3 位为 101,这时它的 thread、epoch、age 都为 0,不保存hashcode信息 - 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,
markword
值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
测试
利用 jol 第三方工具来查看对象头信息
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
new Thread(() -> {
log.debug("synchronized 前");
System.out.println(classLayout.toPrintableSimple(true));
synchronized (d) {
log.debug("synchronized 中");
System.out.println(classLayout.toPrintableSimple(true));
}
log.debug("synchronized 后");
System.out.println(classLayout.toPrintableSimple(true));
}, "t1").start();
}
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中,也就是偏(心)向某个线程了
禁用偏向锁
禁用偏向锁后,创建对象后,最后3位是001,无锁状态。加锁后,变为000,轻量级锁,同时保存了锁记录地址。释放锁后,变回001无锁状态,同时清除锁记录地址
11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
9、偏向锁的撤销
9.1 hashcode
在Dog d = new Dog();
后加上一句 d.hashCode();
- 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
- 调用了
hashCode()
后会撤销该对象的偏向锁
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
因为调用了hashcode()
,但是默认是偏向锁,存储的是线程id,没有内存去存储hashcode
,因此会撤销偏向锁,用来存储hashcode
值
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
9.2 其它线程使用对象
当有其它线程使用偏向锁对象时【没有发生锁竞争】,会将偏向锁升级为轻量级锁
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (TestBiased.class) {
TestBiased.class.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}, "t2");
t2.start();
}
9.3 调用 wait/notify
重量级锁才支持 wait/notify
,调用后,锁直接升级为重量级锁
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
}
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
总结
-
默认情况下,偏向锁是开启的,即这个锁归这个对象所拥有
-
如果有其他线程获取锁或者调用hashcode,那么升级为轻量级锁
-
如果发生锁竞争或者调用
wait/notify
,那么升级为重量级锁
10、批量重偏向、撤销
- 对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 。当(某类型对象)撤销偏向锁超过阈值 20 次后,jvm 会在给(所有这种类型的状态为偏向锁的)对象加锁时重新偏向至新的加锁线程
- 当撤销偏向锁阈值超过 40 次后,jvm 会将整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的
- 例如:当前有40个锁对象,刚开始都偏向t1线程。现在t2线程获取这40个锁对象,1-19个锁对象会撤销偏向锁,第20个锁对象往后,会撤销t1的偏向锁,将偏向锁设置为t2【达到20阈值】。然后t3线程获取这40个锁对象,由于前19个锁对象已经是非偏向锁了,从第20个开始,又会撤销偏向锁,最后撤销次数达到40阈值后,会将所有的锁变为不可偏向的,即使新创建的对象也是不可偏向的。
演示批量重偏向
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
...
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============>
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 // 原始轻量级锁偏向t1
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 // t2获取锁,将锁升级为轻量级锁
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 // 释放锁后,轻量级锁被撤销
...
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 // 第20次,初始轻量级锁偏向t1
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 // 第20次撤销锁,达到阈值,jvm将后边所有锁偏向t2
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
...
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
11、锁消除
锁消除 :JIT
即时编译器会对字节码做进一步优化,下边代码中o
是一个局部变量,不会共享,所以编译后,不会执行加锁操作,而是直接执行x++
public class MyBenchmark {
static int x = 0;
public void b() throws Exception {
//这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除
Object o = new Object();
synchronized (o) {
x++;
}
}
}