synchronized 底层原理
Java对象头
来源
以32位虚拟机为例
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
| (标记字段) | (类型指针) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
其中Mark Word结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
|------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|------------------------------------------------------------------|--------------------|
Monitor(锁)
Monitor: 被翻译为 监视器 或管程
- 刚开始Monitor中Owner为null
- 当Thread-2执行synchronized(obj)就会将Monitor 的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒 EntryList中等待的线程来竞争锁,竞争的时是非公平的
- 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify 时会分析
锁的状态
轻量级锁
锁重入
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),可以用轻量级锁来优化
- 轻量级锁对使用者还是透明的,语法仍然是synchronized
static final Object obj = new Object();
public static void method1(){
synchronized (obj)
{//同步块1
method2();}}
private static void method2() {
synchronized (obj)
{
//同步块2}}
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录结果,内部可以存储锁定对象的Mark Word(标记字段)
-
让锁对象中Object reference 指向,并尝试用cas替换Object的Mark Word,将Mark Word 的值存入锁记录
-
如果cas替换成功,最想头存储了锁记录地址和状态 00 ,表示有该线程给对象加锁
-
如果cas失败
-
如果是其他线程已经持有该Object的轻量级锁,表明有竞争,进入锁膨胀过程
-
如果是同一线程执行了 synchronized 锁重入 ,那么在天机一条Lock Record作为锁重入的计数
-
当退出synchronized代码块时(解锁时),如果有取值位null的锁记录,表示有锁重入,只是重置锁记录,表示锁重入数减一
-
当退出synchronized代码块时(解锁时),锁记录部位null ,这时使用cas将 Mark World的值回复给对象头
- 成功,则结果成功
- 失败,说明轻量级锁进行了锁膨胀的过程或已经升级为重量级锁,进入重量级锁解锁流程
-
锁膨胀
- 如果尝试加轻量级锁的过程中,CAS无法成功,这是有其他线程未次对象加上了轻量级锁(有竞争),这是需要进行所鹏展,将轻量级锁变成重量级锁。
-
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
-
这是Thread-1加轻量级锁失败,进入做膨胀过程
- Object对象申请Monitor锁,让Object指向重量级锁的地址
- 然后自己进入Moniter的RntryList BOLOCED
-
当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,如果失败,这是会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,是指Owner为null ,唤醒EntryList中的BLOCKED线程
-
自旋优化
重量级锁竞争的时候,还可以用自旋来优化,如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁)这时当前线程可以避免阻塞进行上下文切换(上下文切换耗费性能)。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
偏向锁
- 轻量级锁在没有竞争时,每次重入依然需要执行CAS操作
- Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向状态
|-------------------------------------------------------------------|-------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------------------|-------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------------------|-------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------------------|-------------------|
| ptr_to_lock_record:62 | 00 |Lightweight Locked |
|-------------------------------------------------------------------|-------------------|
| ptr_to_heavyweight_monitor:62 | 10 |Heavyweight Locked |
|-------------------------------------------------------------------|-------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------------------|-------------------|
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
锁撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
原文
-
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销,在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
- 偏向锁不会记录hashcode
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
-
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
-
wait/notify 时重量级锁使用的方法 ,调用时偏向锁升级为重量级锁
批量重偏向
以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作是,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
- 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
- JIT 即时编译器,???
wait notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
obj.wait()// 让进入 object 监视器的线程到 waitSet 等待
wait(long n) //有时限的等待, 到 n 毫秒后结束等待,或是被 notify
obj.notify() //在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() //让 object 上正在 waitSet 等待的线程全部唤醒
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
TimeUnit.SECONDS.sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
20:40:04 [Thread-3] c.Test1 - 执行....
20:40:04 [Thread-4] c.Test1 - 执行....
20:40:06 [main] c.Test1 - 唤醒 obj 上其它线程
20:40:06 [Thread-3] c.Test1 - 其它代码....
wait notify 的正确姿势
sleep(long n) 和 wait(long n) 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4) 它们状态 TIMED_WAITING
保护式暂停模式
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get(Long timeOut) { //超时时间
// 1) 记录最初时间
long begin = System.currentTimeMillis();
// 2) 已经经历的时间
long timePassed = 0;
synchronized (lock) {
// 条件不满足则等待
while (response == null) { //防止虚假唤醒
long waitTime = timeOut - timePassed; //计算剩余等待的时间
if (waitTime <= 0) {
log.debug("break...");
break;
}
try {
lock.wait(waitTime); //防止虚假唤醒后 等待时间大于输入时间
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3) 记录已经经历的时间
timePassed = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
// 子线程执行下载
List<String> response = download();
log.debug("download complete...");
guardedObject.complete(response);
}).start();
log.debug("waiting...");
// 主线程阻塞等待
Object response = guardedObject.get();
log.debug("get response: [{}] lines", ((List<String>) response).size());
}
21:13:15 [main] c.Test1 - waiting...
21:13:17 [Thread-3] c.Test1 - download complete...
21:13:17 [main] c.Test1 - get response: [2] lines
join原理
利用了保护式暂停模式
long totalWaited = 0;
long totalWaited = 0; // 记录共计等待时间
long toWait = timeoutInMilliseconds; //记录约定等待时间
boolean timedOut = false; //记录是否超时
if (timeoutInMilliseconds == 0 & nanos > 0) {
// We either round up (1 millisecond) or down (no need to wait, just return)
if (nanos < 500000)
timedOut = true;
else
toWait = 1;
}
while (!timedOut && !isDead()) { //防止虚假环境,并且判断是否超时
long start = System.currentTimeMillis(); //记录等待开始时间
wait(toWait); // 开始等待 第一次等待时间为约定等待时间 第二次为剩余等待时间
long waited = System.currentTimeMillis() - start; //计算本次循环等待时间
totalWaited+= waited; //计算共计等待时间
toWait -= waited; //计算等待时间
// Anyone could do a synchronized/notify on this thread, so if we wait
// less than the timeout, we must check if the thread really died
timedOut = (totalWaited >= timeoutInMilliseconds); //超时判断
}
park & Unpark (搁置、推迟)
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
示例
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park(); //线程进行WAIT状态
log.debug("resume...");
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
park unpark 原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter(计算器) , _cond(状态) 和 _mutex (互斥锁)
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
5. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
6. 唤醒 _cond 条件变量中的 Thread_0
7. Thread_0 恢复运行
8. 设置 _counter 为 0
9. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
10. 当前线程调用 Unsafe.park() 方法
11. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
12. 设置 _counter 为 0