一、对象头
1. 简介
- 以32位的 JVM 为例,每个对象头都包含了如下信息
# 0 组成
Mark Word: 锁的信息,hashcode,垃圾回收器标志
Klass Word: 指针,包含当前对象的Class对象的地址
# 1. 普通对象,占用8个字节,64位
Object Header (64 bits)
Mark Word(32 bits) Klass Word(32 bits)
# 2. 数组对象, 占用12个字节, 96位 包含额外的4个字节用来保存数组长度
Object Header (96 bits)
Mark Word(32 bits) Klass Word(32 bits) Array Length (32 bits)
2. Mark Word
- 01/00/11: 代表是否加锁
- age: 垃圾回收器标记
二、 Monitor监视器
1. monitor
- 由 《操作系统》提供,又叫监视器或者管程
- 包含三部分
1. Owner: 保存当前获取到java锁的,线程指针
2. EntryList: 保存被java锁的锁阻塞的,线程指针
3. WaitSet: 保存被java锁等待的,线程标记
- synchronized 的java对象,会被关联到一个monitor监视器,java对象头的Mark Word就被设置为
monitor监视器的地址
- 重量级锁
- 非公平锁
2. 竞争步骤
- java对象被synchronized修饰后
- 当它获取到对象锁的时候,该对象就会被关联到monitor,java对象的Mark Word就会变为 prt_to_heavyweight_monitor, 即是用来保存重量级锁的地址
1. thread-1 通过synchronized获取到一个obj对象
1.1 将obj对象头中的Mark Word变为prt_to_heavyweight_monitor(30 bit)(monitor指针)
1.2 在owner中保存obj对象头的hashcode等信息
1.3 obj对象头的锁状态变为 10(重量级锁)
1.4 根据monitor指针,将Owner设置为thread-1
2. thread-2 过来后,检查obj锁对象头
2.1 发现该obj对象头的Mark Word的锁状态已经是重量级锁
2.2 根据Mark Word中锁的地址检查到当Owner已经有其他线程了
2.3 thread-2进入到EntryList,进行Block
3. thread-1 执行完临界区代码后,
3.1 monitor的Owner进行清空
3.2 将owner中的当前线程的owner和obj对象头中的monitor地址再次交换
3.3 monitor唤醒EntryList中其他线程
3.4 其他在 EntryList 中等待的线程, 再次竞争对象锁,再次设置monitor的Owner
a. synchronized(obj),就会有一个monitor监管该对象
b. 同步代码块如果发生异常时候,也会将锁释放
c. synchronized(obj), 必须关联到同一个obj,不然就不会指向同一个monitor
三、常见锁
1. 轻量级锁
- 一个对象虽被多个线程访问,但访问时间错开,不存在竞争
- 轻量级锁对使用者 是透明的, 语法:syncronized
- 当存在其他线程竞争的时候,自动升级为重量级锁
2. 锁重入
- 一个线程在调用一个方法的时候,在调用链中,多次使用同一个对象来加锁
# 加锁
1. 创建锁纪录对象
- 线程在自己的工作内存内,创建栈帧,活动栈帧创建一个 《锁记录》 的结构
- 锁记录对象:是在JVM层面的,对用户无感知
- 锁记录: lock record: 加锁的信息,用来交换对象头的mark word, 00 代表轻量级锁状态
Object Reference: 锁对象的指针
# Object Body: 该锁对象的成员变量
2.1 cas交换成功(compare and set)
# 01 代表无锁, 00代表轻量级锁
- 将Object reference 指向锁对象的地址
- 尝试cas交换Object中的 Mark Word和栈帧中的锁记录
- Object对象头中,交换后变为00,代表该对象现在是轻量级锁
2.2 cas失败
- 情况一:锁膨胀,若其他线程持有该obj对象的轻量级锁,表明有竞争,进入锁膨胀过程,加重量级锁
- 情况二:锁重入,若本线程再次synchronized锁,再添加一个Lock Record作为重入计数
- 两种情况区分: 根据obj中保存线程的lock record地址来进行判断
null: 表示重入了几次
解锁
- 退出synchronized代码块时,若为null的锁记录,表示有重入,这时清除锁记录(null清楚)
- 退出synchronized代码块时,锁记录不为null,cas将Mark Word的值恢复给对象头
同时obj头变为00无锁状态
- 成功则代表解锁成功; 失败说明轻量级锁进入了锁膨胀
3. 锁膨胀
- 在尝试轻量级加锁时,cas无法成功
- 可能因为:其他线程为此对象加上了轻量级锁(有竞争),这时进行锁膨胀,锁变为重量级锁
- 轻量级锁没有阻塞机制
加锁
- 当thread-1进行轻量级加锁时,thread-0已经为该对象加了轻量级锁
进入锁膨胀
- thread-1轻量级加锁失败,进入了锁膨胀流程。要进行阻塞(只有重量级锁才有)
申请monitor锁
- 为Object对象申请monitor锁,并让Object的mark word 指向重量级锁地址
- 然后自己进入monitor的EntryList 进行 Block
解锁
- 当Thread-0 退出同步块时,使用cas将Mark Word的值恢复给对象头,失败进入重量级解锁流程
- 按照Monitor地址找到Monitor,设置Owner为null,唤醒EntryList中BLOCKED线程
4. 自旋优化-重量级锁
- 一个线程的锁被其他线程持有时,该线程并不会直接进入阻塞
- 先本身自旋,同时查看锁资源在自旋优化期间是否能够释放 《避免阻塞时候的上下文切换》
- 若当前线程自旋成功(即此时持有锁的线程已经退出了同步块,释放了锁),这时线程就避免了阻塞
- 针对重量级锁而言
- 10: 重量级锁
智能自旋:
- 自适应的: Java6之后,对象刚刚的一次自旋成功,就认为自旋成功的概率大,就会多自旋几次
反之,就少自旋几次甚至不自旋
- 自旋会占用cpu时间,单核自旋就是浪费,多核自旋才有意义
- java7之后不能控制是否开启自旋功能
- 不公平的锁
5. 偏向锁
- 轻量级锁在没有竞争的时候,发生锁重入的时候,依然需要执行CAS检查,存在性能损耗
- 锁重入时,会产生多条锁记录,作为锁记录
- Java6之后,第一次发生CAS后,将《线程ID》设置到锁对象的Mark Word头上
而不会将Mark Word和轻量级锁的锁记录进行交换,
- 后续重入,只要发现是该线程的就不会再进行CAS检查
- 只要不发生竞争,这个锁就让该线程一直使用
- 调用锁对象的hashcode,会撤销掉偏向锁
- hashcode默认是0,第一次调用的时候就会产生对应的hashcode
6. 锁消除
- Java的 JIT, 即时编译器,对热点代码进行优化
- 逃逸分析: JVM 是根据锁对象是否可以发生逃逸分析来判断
- JVM默认开启锁消除机制
- Java中锁消除默认是打开的,会根据代码中锁关联的对象是否能够逃逸决定是否优化
- 关闭锁消除: Java -XX: -EliminateLocks -jar demo.jar
6.1 消除
package com.nike.erick.d01;
public class Demo07 {
public static void main(String[] args) {
lockMethod();
nonLockMethod();
}
/*虽然此时加了synchronized, 但是代码在执行的时候
* 1. 并不存在多线程同步访问的场景,所以synchronized 被JIT优化掉了*/
private static void lockMethod() {
long startTime = System.currentTimeMillis();
/*做成包装类,来增加演唱时间*/
Integer number = 0;
for (int i = 0; i < 10000000; i++) {
synchronized (new Object()) {
number++;
}
}
System.out.println("Lock Method Times: " + (System.currentTimeMillis() - startTime));
}
private static void nonLockMethod() {
long startTime = System.currentTimeMillis();
Integer number = 0;
for (int i = 0; i < 10000000; i++) {
number++;
}
System.out.println("Non Lock Method Times: " + (System.currentTimeMillis() - startTime));
}
}
6.2 逃逸分析
- 如果锁对象可能逃逸,那么就不会进行锁优化
private static void lockMethod() {
boolean flag = true;
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
System.out.println("逃逸代码块");
}
}).start();
long start = System.currentTimeMillis();
/**
* 上面锁逃逸,所以并不会进行锁消除
*/
for (int i = 0; i < 100000000; i++) {
synchronized (lock) {
flag = !flag;
}
}
System.out.println("加锁:" + (System.currentTimeMillis() - start));
}
四、不相干锁
1. 锁粒度细化 – 多把锁
- 一间屋子两个功能:睡觉,学习,互不影响,
- 如果用一个屋子(一个对象锁)的话,并发度很低
// 互不影响的功能
package com.dreamer.multithread.day04;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
BigRoom room = new BigRoom();
// 睡觉的一类线程
Thread firstSleep = new Thread(() -> room.sleep());
Thread secondSleep = new Thread(() -> room.sleep());
// 工作的一类线程
Thread firstWork = new Thread(() -> room.work());
Thread secondWork = new Thread(() -> room.work());
firstSleep.start();
secondSleep.start();
firstWork.start();
secondWork.start();
firstSleep.join();
secondSleep.join();
firstWork.join();
secondWork.join();
// 一共需要 2*2+2*2 = 8s
System.out.println("total time:" + (System.currentTimeMillis() - start));
}
}
class BigRoom {
/*下面两个方法,永远不会在同一个时间调用,因此用同一把锁,浪费资源*/
public void sleep() {
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void work() {
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 一个对象中,如果不同种类方法只会被同一种线程调用,则可以进行锁粒度细化
- 如果多把锁,被一个方法同时使用了,可能造成死锁
// 执行上面的方法,只需要4s
class BigRoom {
private Object sleepLock = new Object();
private Object workLock = new Object();
/*下面两个方法,永远不会在同一个时间调用,因此用同一把锁,浪费资源*/
public void sleep() {
synchronized (sleepLock) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void work() {
synchronized (workLock) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2. 锁活跃
2.1 死锁
- 线程一:持有a锁,等待b锁
- 线程二:持有b锁,等待a锁
- 互相等待引发的死锁问题
- 哲学家就餐问题
- 定位死锁: 可以借助jconsole来定位死锁
- 解决方法: 都按照相同顺序加锁就可以,但可能引发饥饿问题
package com.dreamer.multithread.day04;
import java.util.concurrent.TimeUnit;
public class Demo02 {
public static void main(String[] args) {
BigRoom room = new BigRoom();
new Thread(() -> room.sleepAndWork()).start();
new Thread(() -> room.workAndSleep()).start();
}
}
class BigRoom {
private final Object sleepLock = new Object();
private final Object workLock = new Object();
// 互相持有对方的锁
public void sleepAndWork() {
synchronized (sleepLock) {
consumeTime();
synchronized (workLock) {
System.out.println("睡醒---工作啦");
}
}
}
public void workAndSleep() {
synchronized (workLock) {
consumeTime();
synchronized (sleepLock) {
System.out.println("工作后--要睡觉啦¬");
}
}
}
private void consumeTime() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 饥饿锁
- 某个线程因为优先级太低,一直得不到cpu的执行
2.3. 活锁
- 两个线程中互相改变对方结束的条件,导致两个线程一直运行下去
- 可能会结束,但是二者会交替进行
package com.dreamer.multithread.day04;
public class Demo04 {
private static int counter = 10;
public static void main(String[] args) {
new Thread(() -> {
while (counter < 20) {
counter++;
System.out.println(" ++ 操作:" + counter);
}
}).start();
new Thread(() -> {
while (counter > 0) {
counter--;
System.out.println(" -- 操作:" + counter);
}
}).start();
}
}