概述
synchronized是Java的一个关键字,用来保证多线程下临界区资源的共享安全性
synchronized可以加在方法上(静态方法和普通方法)、代码块上
使用语法:
synchronized (对象) {
// 操作临界资源
}
public synchronized void test() {
// 操作临界资源
}
为什么需要加synchronized?
由于在多线程环境下,对于共享资源(比如共享变量)的修改,可能会导致结果与预期不一致
产生原因:以下有两个线程,分别对static变量进行多次+1和-1操作,最终结果预期为0,但由于单条java指令被编译成字节码后可能对应多条指令,因此会造成数据出错
示例:
static int sum = 0;
static Object object = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 500; i++) {
sum++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 500; i++) {
sum--;
}
});
t1.start();
t2.start();
t1.join(); // 等待t1线程执行完成
t2.join(); // 等待t2线程执行完成
log.debug("{}", sum); // 最后结果不确定
i++对应字节码为:
getstatic i // 取指令
iconst_1
iadd // 加1操作
putstatic i // 存指令
i--对应字节码为:
getstatic i
iconst_1
isub
putstatic i
假如第一个线程取完指令并进行+1操作后,还没来得及存指令;此时发生了线程上下文切换,第二个线程也取指令并完成了-1操作。
此时无论是第一个线程先将1写回变量,还是第二个线程将-1写回变量,结果都不等于0
因此,需要有一种方法保证两个线程不能同时操作临界资源,synchronized就可以解决这样的问题。
Monitor监视器(管程)
首先,synchronized无论是加在方法上,还是加在代码块上,本质上都跟Java中的一个类相对应。
假如存在一个Main类,synchronized加在不同位置时对应的类:
- synchronized void test():加在普通方法上,对应类实例,相当于Main.this
- synchronized static void test():加在静态方法上,对应类对象Main.class
- synchronized (obj):加在任意其他对象上,对应给对象的实例,即obj
因此,Java中的每一个类都需要能够让线程互斥地访问临界资源,当一个线程正在使用临界资源时,其他线程需要处于阻塞状态。
由于需要记录正在使用临界资源的线程、处于阻塞状态的线程、处于等待状态的线程(由于运行条件不满足主动等待的线程),每个类都需要关联一种程序结构,这种程序结构就叫做:管程
从上述得出:
管程需要存储3种不同状态的线程:
- Owner:正在执行的线程
- EntryList:未获取到锁而处于阻塞状态的线程
- WaitSet:处于等待状态的线程
类是如何与管程建立关联的?
每个Java中的类都会包含头部header,header中包含了32bit的MarkWord字段
Java中的类就是通过MarkWord字段与管程相关联的
ptr_to_heavyweight_monitor:指向管程(Monitor)的指针
此时每一个Java类都可以对多个线程进行管理了。
Monitor工作流程
- 初始时,Monitor的Owner为null
- 当有线程thread2进入时,会将Monitor中的Owner设置为thread2(Monitor中只能有一个Owner)
- 如果此时有其他线程thread3,thread4进入synchronized方法,就会进入Monitor的EntryList中,进入阻塞状态
- Owner中的线程执行完,会唤起EntryList中等待的线程来竞争锁(非公平)
- WaitSet中的thread0,thread1是之前获取过锁,但条件不满足进入WAITING状态的线程
锁升级
synchronized锁的4种状态:
- 无锁(Normal):MarkWord后三bit为 001
- 偏向锁(Biased):MarkWord后三bit为 101
- 轻量级锁(Lightweight Locked):MarkWord后两bit为 00
- 重量级锁(Heavyweight Locked):MarkWord后两bit为 10
具体处于哪种状态下,是由锁住的对象的MarkWord决定的
轻量级锁
假如默认情况下,synchronized锁对象为无锁状态,此时多个线程运行method1方法(但时间是错开的)
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块
}
}
此时线程Thread-0尝试获取锁(假如当前Object对象是无锁状态)
左侧部分代表线程运行时内存栈帧,右侧为加锁的对象
为了标记是谁对当前对象加了锁,Object类会将锁记录(Lock Record)地址写入MarkWord中,并将MarkWord中的值写入Lock Record中,便于后续退出synchronized代码块后恢复初始状态,这种比较并互换值的操作就叫做CAS(compare and swap)
如果CAS成功,那么此时Object对象的MarkWord就更换了,其他线程看到MarkWord的后两bit为00时,就知道已经有人占用了锁,需要进入阻塞状态
偏向锁
但假如此时线程0内部有锁重入的现象:
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块
}
}
虽然是同一个线程获取锁,但由于不同方法对应不同栈帧(即Lock Record不同),会导致CAS失败
- 如果当前是其他线程持有了Object锁,表明有竞争,则会进入锁膨胀的过程,升级为重量级锁
- 如果是当前线程持有了Object锁,则会添加一条Lock Record作为锁重入的计数
产生问题的原因为:Object对象MarkWord存储的是Lock Record地址,CAS机制是通过判断Lock Record是否相同来判断是否CAS成功
优化措施:MarkWord存储线程ID(Thread ID),这样即使是不同方法下,由于Thread ID相同,也能判断对象锁的持有者是否是自己。在Java6中这种锁被引入,并称为偏向锁
重量级锁
如果同时有多个线程竞争同一把锁的情况,此时MarkWord已经无法记录多个线程的竞争情况,需要进行锁膨胀,将轻量级锁升级为重量级锁
重量级锁结构:
- MarkWord中不再存储线程信息,而是存储指向Monitor的指针
- Monitor存储线程信息:获取到锁的线程存储在Owner中,阻塞的线程存储在EntryList中,等待的线程存储在WaitSet中
每当Owner中的线程执行完退出同步块,就会唤起EntryList中阻塞的线程,将锁分配给该线程