本文使用的是JDK1.8
目录
[Mark Word](#Mark Word)
引言
对于synchronized原理讲解之前,我们需要知道Java对象在JVM中的结构和Monitor是什么。
参考文章:Java对象头详解 - 简书 (jianshu.com)
Java对象在JVM的结构
普通对象
数组对象
其中对象头和加锁关系非常大。所以这里只介绍对象头。
对象头
以32位的JVM为例,如果是64位的,那就×2.
普通对象
ruby
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
ruby
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
Mark Word
ruby
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
64位的如下:
偏向锁位 + 锁标志位
identity_hashcode :hashcode。只有调用该方法时才会生成。如果调用某个对象未被重写的HashCode方法,此时对它进行上锁,它将直接进入轻量级锁;如果它在偏向锁的基础上,在调用HashCode方法,此时它就变成重量级锁了。
epoch:偏向时间戳。
Monitor
上面提到了重量级锁,就是和这里的Monitor对象有关。每个Java对象都可以关联一个Monitor对象,使用synchronized加锁的对象如果升级成重量级锁,就要和Monitor对象关联了。
Monitor对象主要由三部分构成
Owner
开始时,Owner为空。如果对象A被Thread-1线程加成了重量级锁,并且它右调用了Object中的wait方法,则它就进入WaitSet中等待,此时其他线程可以对对象A上锁了。Thread-1同理。现在就是Thread-2对对象A上锁了,并且没释放,也没有wait。
EntryList
Thread-3和Thread-4线程也想对对象A上锁,但是此时Thread-2线程持有锁,它俩只能进入EntryList进行等待。如果后续Thread-2线程释放了锁,就会通知EntryList中所有的线程来竞争锁。
WaitSet
原本持有锁,但是使用wait方法后放弃了锁,就进入WatiSet中进行等待。后续只能用对象A的notify()或者notifyAll()方法来唤醒它们。
加锁过程
synchronized的加锁过程是逐步提高的,并不是一上来就要加重量级锁。
锁消除
对于一些对象,如果它不可能被其他线程贡献,并且对于该对象使用synchronized加锁了,那么JIT(即时编译器)会自动的不给这个对象加锁。因为不可能发生锁冲突的情况。如下代码:
java
public class MyTest {
static int x = 0;
public void a() throws Exception {
x++;
}
// 这里的 o 对象是不可能被其他线程使用到的
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
锁消除是默认开启的。-XX:-EliminateLocks 使用它关闭。
偏向锁
- 偏向锁是默认开启的。所以当对象创建后,它的锁标志位后三位为 101,且偏向时间戳为0,也没有线程指针。
- 偏向锁默认是延迟的。也就是不会在程序启动时立刻生效,如果不想有延迟,可以添加VM参数 -XX:BiasedLockingStartupDelay=0 来禁用。
偏向锁使用
在没有锁竞争的时候,每次重入都需要进行CAS操作(把线程ID记录到对象中),但这个操作在第一次执行完之后如果重入的时候在使用CAS操作没必要。所以在JDK 6 之后就使用偏向锁来优化。
线程第一次使用对象后,就把线程ID记录的对象头中的Mark Word中。后续如果要加锁,先看看线程ID是不是自己,表示重入,就没发生竞争。
重偏向
如果有多个线程 访问同一个对象 ,但是没有发生锁竞争。比如线程1先对对象A加了偏向锁(线程A已经结束),后续线程2又使用了对象A,当访问次数超过20 次后,后续如果线程2还要使用对象A,那么此时对象A的Mark Word中的线程ID就变成了线程2的。
撤销偏向
- 当上述的重偏向次数超过 40 次后,那么这个类所创建的对象都会变成不可偏向的,新建的对象也都是不可偏向的。
- 当调用对象的hashCode方法时,偏向锁也会被撤销。如果是轻量级锁,那么hashCode会保存在锁记录中;如果是重量级锁,hashCode会保存在Monitor中。
- 线程1使用对象时,线程2也来使用相同的对象了,此时也会撤销偏向锁,升级成轻量级锁。
- 当调用了wait方法后,在notify后,此时就从偏向锁升级成重量级锁了。
轻量级锁
之前谈到,当多个线程对同一对象操作时,锁状态会从偏向锁升级到轻量级锁。轻量级锁是对使用者透明的。
轻量级锁加锁过程如下:
- 每个线程的栈帧都要创建一个**锁记录(Lock Record)**的结构,其内部存储锁定对象的Mark Word
- Object reference指向锁对象;并用CAS尝试把对象头中的Mark Word中的跟偏向锁相关的内容存到lock record中,把原来的Mark Word中偏向锁的记录改成存 lock record的地址。如果操作成功,就如下图所示:
- 如果CAS尝试失败:
- 可能是其他线程拥有了该对象的轻量级锁。此时就会自选优化,最后升级到重量级锁
- 也有可能是自己这个线程执行的所重入,那么就会继续增加一条锁记录,不过新加的锁记录的指向地址就是空,后续取到为空的锁记录时,重入记录减一。如下图
- 轻量级锁解锁时,如果最后一条锁记录为 偏向锁的相关信息,则使用CAS把Mark Word中的恢复
- 恢复成功,就是解锁成功
- 恢复失败,说明轻量级锁升级为重量级锁了,要用重量级锁的方式来解锁。
重量级锁
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
下图中的Thread-0线程已经加了轻量级锁,当Thread-1线程想来加锁时,那就不成功,Object对象就进入锁升级,升级到重量级锁,也就要和刚开始提到的Monitor关联。
Thread-1线程要为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList 进行 BLOCKED。
自旋优化
上面Thread-1线程进入EntryList中不是立刻的。它还对Thread-0线程抱有一丝希望,觉得它能马上执行完成,然后释放锁。所以Thread-1在此期间就重试加锁,过程如下:
重试成功:
重试失败:
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能。