⭐ 作者:小胡_不糊涂
🌱 作者主页:小胡_不糊涂的个人主页
📀 收录专栏:JavaEE
💖 持续更文,关注博主少走弯路,谢谢大家支持 💖
1. 特性
1.1 互斥
synchronized 会起到互斥效果,某个线程执⾏到某个对象的 synchronized 中时,其他线程如果也执⾏到同⼀个对象synchronized 就会阻塞等待。
synchronized⽤的锁是存在Java对象头⾥的。可以粗略理解成,每个对象在内存中存储的时候,都存有⼀块内存表⽰当前的 “锁定” 状态(类似于厕所的 “有⼈/⽆⼈”)。
如果当前是 “⽆⼈” 状态,那么就可以使⽤,使⽤时需要设为 “有⼈” 状态;如果当前是"有⼈"状态,那么其他⼈⽆法使⽤,只能排队。
针对每⼀把锁,操作系统内部都维护了⼀个等待队列。当这个锁被某个线程占有的时候,其他线程尝试进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程,再来获取到这个锁。
注意:
- 当上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁,⽽是要靠操作系统来 “唤醒”,这也就是操作系统线程调度的⼀部分⼯作。
- 假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁,然后 C 再尝试获取锁,此时 B 和 C都在阻塞队列中排队等待。但是当 A 释放锁之后,虽然 B ⽐ C 先来的,但是 B 不⼀定就能获取到锁,⽽是和 C 重新竞争,并不遵守先来后到的规则。
1.2 可重入
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题。
什么情况下,会把自己锁死?
⼀个线程没有释放锁,然后⼜尝试再次加锁。
// 第⼀次加锁,加锁成功
// 第⼆次加锁,锁已经被占⽤,阻塞等待。
按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待。直到第⼀次的锁被释放,才能获取到第⼆个锁。
但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进⾏解锁操作。这时候就会 死锁,这样的锁也就称为 不可重入锁。
在可重⼊锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息。
- 如果某个线程加锁的时候,发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器自增。
- 解锁的时候计数器递减为 0 的时候,才真正释放锁。(才能被别的线程获取到)
2. 使用
synchronized 本质上要修改指定对象的 “对象头”。从使⽤⻆度来看,synchronized 也势必要搭配⼀个具体的对象来使⽤。
2.1 修饰代码块
明确指定锁哪个对象
//锁任意对象
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
//锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2.2 修饰普通方法
锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2.3 修饰静态方法
锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
两个线程竞争同⼀把锁,才会产⽣阻塞等待。
两个线程分别尝试获取两把不同的锁,不会产⽣竞争。
3. 锁机制
3.1 锁升级
JVM 将 synchronized 锁分为 ⽆锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进⾏依次升级。
- 偏向锁
第⼀个尝试加锁的线程,优先进⼊偏向锁状态。
偏向锁不是真的加锁,⽽只是在锁的对象头中记录⼀个标记(记录该锁所属的线程)。如果没有其他线程参与竞争锁,那么就不会真正执⾏加锁操作,从⽽降低程序开销。⼀旦真的涉及到其他的线程竞争,再取消偏向锁状态,进⼊轻量级锁状态。
偏向锁本质上相当于 “延迟加锁” 。能不加锁就不加锁,尽量来避免不必要的加锁开销。 - 轻量级锁
随着其他线程进⼊竞争,偏向锁状态被消除,进⼊轻量级锁状态(⾃适应的⾃旋锁)。
此处的轻量级锁就是通过 CAS 来实现。
通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤)
如果更新成功,则认为加锁成功;
如果更新失败,则认为锁被占⽤,继续⾃旋式的等待(并不放弃 CPU)。
此处的⾃旋不会⼀直持续进⾏,⽽是达到⼀定的时间/重试次数,就不再⾃旋了,也就是所谓的 “⾃适应”。
- 重量级锁
如果竞争进⼀步激烈,⾃旋不能快速获取到锁状态,就会膨胀为重量级锁
此处的重量级锁就是指⽤到内核提供的 mutex。
- 执⾏加锁操作,先进⼊内核态
- 在内核态判定当前锁是否已经被占⽤
- 如果该锁没有占⽤,则加锁成功,并切换回用户态
- 如果该锁被占⽤,则加锁失败。此时线程进⼊锁的等待队列,挂起-等待被操作系统唤醒.
- 当这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒这个线程,尝试重新获取锁
3.2 锁消除
锁消除是一种编译器优化的手段。
编译器+JVM 判断锁是否可消除。如果可以,就直接消除。
有些应⽤程序的代码中,⽤到了 synchronized。例如 StringBuffer:
StringBuffer str=new StringBuffer();
str.append("a");
str.append("b");
这里的每个 append 的调⽤都会涉及加锁和解锁,但如果只是在单线程中执⾏这个代码,编译器就会自动把synchronized优化掉。
3.3 锁粗化
⼀段逻辑中如果出现多次加锁解锁,编译器 + JVM 会⾃动进⾏锁的粗化。