synchronized中的锁
1.1 Java对象头
以32位虚拟机为例
普通对象的对象头包括下面两个部分
Object Header(64 bits) | |
---|---|
Mark Word(32 bits) | Klass Word(32 bits) |
数组对象的对象头包括下面三个部分
Object Header(96 bits) | ||
---|---|---|
Mark Word(32 bits) | Klass Word(32 bits) | array length(32 bits) |
其中Mark Word 结构为
Mark Word(32 bits) | State |
---|---|
hashcode:25 | (分代年龄)age:4 | (偏向锁是否启用)biased_lock:0 | 01 | Normal |
thread:23 | epoch:2 | age:4 | biased_lock:0 | 01 | Biased |
ptr_to_lock_record:30 | 00 | Lightweight Locked |
ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
11 | Marked for GC |
1.2 Monitor(锁)工作原理
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后
该对象头的Mark Word 中就被设置指向Monitor对象的指针
monitor结构如下
那和java对象有什么关系呢?
-
每一个java对象都会关联一个Monitor对象(当调用synchronized关键字尝试给对象加锁时就会关联)
-
当线程进入synchronized代码块时,Markword会指向Monitor,Owner的值就会是当前线程
-
如果此时再来一个Thread1时,发现Owner有值,Thread1就获取不了锁
-
Thread1此时就会进入EntryList,可以理解为阻塞队列,Thread1进入阻塞状态
-
Thread2执行完成后,就会Owner指向EntryList里面某个线程
注意:
-
synchronized 必须是进入同一个对象的monitor才有上述的效果
-
不加synchronized 的对象不会关联监视器,不遵从以上规则
1.3 synchronized 优化原理
1.3.1 轻量级锁
-
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
-
轻量级锁对使用者是透明的,即语法仍然是synchronized
-
假设有两个方法同步块,利用同一个对象加锁
-
static final Object obj = new Object(); public static void method1(){ synchronized (obj){ method2(); } } private static void method2() { synchronized (obj){ } }
-
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
-
进入menthod1中的synchronized时,让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录(00表示轻量级锁,上面表格中写过)
-
如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
-
如果cas失败,有两种情况
-
如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程(后面会提到)
-
如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
-
这里就是说明了synchronized是可重入锁
-
-
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录
表示重入计数减一 -
当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
1.3.2 锁膨胀
- 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static final Object obj = new Object();
public static void method1(){
synchronized (obj){
method2();
}
}
-
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
-
这时Thread-1加轻量级锁失败,进入锁膨胀流程
- 即为object对象申请 Monitor锁,让object指向重量级锁地址
- 然后自己进入Monitor的EntryList BLOCKED
-
当Thread-O退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
这里就和前面的知识串联起来了
1.3.3 自旋优化
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋重试成功的情况:(适用于多核CPU)
线程1 | 对象Mark | 线程2 |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 | 10(重量锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功(加锁) |
- | 10(重量锁)重量锁指针 | 执行同步块 |
- | … | … |
- 自旋重试失败的情况:
线程1 | 对象Mark | 线程2 |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 阻塞 |
- | … | … |
- 在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
- Java 7之后不能控制是否开启自旋功能
1.3.4 偏向锁
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作
- Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
- 例如:
static final Object obj = new Object();
public static void method1(){
synchronized (obj){
method2();
}
}
private static void method2() {
synchronized (obj){
}
}
private static void method3() {
synchronized (obj){
}
}