美团技术文档 - 线程锁总结
出于保护数据同步的目的,在多线程操作时对于共享资源需要引入锁的概念,锁的选型会直接的影响系统的性能。
1. 互斥锁
what: 让线程互斥的访问共享资源,同一时间只会有一个线程能对该资源进行访问
why: 对于线程1 写操作;线程2 读的操作,如果同时进行,读到的可能就不是最新的值
how:
- 两种状态: (locked)状态 & (unlocked)状态
- 加锁(acquire):当互斥锁处于未加锁状态时,则加锁成功(把锁设置为已加锁状态),并返回;当互斥锁处于已加锁状态时,那么试图对它加锁的线程会被阻塞,直到该互斥量被解锁。
- 解锁(release):通过把锁设置为未加锁状态释放锁,其他因为申请加锁而陷入等待的线程,将获得执行机会。如果有多个等待线程,只有一个会获得锁而继续执行。
notice: 在访问资源前申请锁访问 - 操作后 - 释放锁2. 读写锁
what: 与互斥锁一直,多了一种状态,加读锁、加写锁和释放锁
why:说白了就是读锁的状态下不能写锁,反之一致,在有读锁的情况,如果还有读的行为可以继续读,写锁就不可以
how:读写锁适合对共享资源访问“读大于写”的场合。读写锁也叫“共享互斥锁”,多个读线程可以并发访问同一资源,这对应共享的概念,而写线程是互斥的,写线程访问资源的时候,其他线程无论读写,都不可以进入临界代码区。
notice: 一般写的权重会高于读,如果一直有线程来读数据,读锁一直被占用,那么写锁会“饿死”。3. 自旋锁
what: 线程在acquire自旋锁失败的时候,不会主动让出CPU从而进入睡眠等待状态,而是会一直忙等占着CPU,然后更快的准备测试和设置指令,直到成功获得执行,因为自旋锁会期待线程持有锁后快速释放锁。
why: 内核态线程的部分线程不能睡眠,需要持续去持有资源,例如中断函数等;用户态的线程则不推荐了,因为等待过程属于浪费资源,因为用户态的线程并不能保证很快的release锁,此处的资源浪费就违背了自旋锁的概念。
notice: 自旋锁必须在多核架构下,否则无论什么态,一直占有所有线程都等待,全“饿死”了。锁的范围
尽可能小的范围,减少持有时间死锁
ABBA锁

时序图
HOW:
--
public class ABBADeadlock {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程A: 先A后B
Thread threadA = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程A: 持有锁A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("线程A: 尝试获取锁B...");
synchronized (lockB) { // ❌ 阻塞在这里
System.out.println("线程A: 获取锁B成功");
}
}
});
// 线程B: 先B后A(ABBA顺序!)
Thread threadB = new Thread(() -> {
synchronized (lockB) {
System.out.println("线程B: 持有锁B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("线程B: 尝试获取锁A...");
synchronized (lockA) { // ❌ 阻塞在这里
System.out.println("线程B: 获取锁A成功");
}
}
});
threadA.start();
threadB.start();
}
}解决方案

// 修正后的线程B:也按A→B顺序
Thread threadBFixed = new Thread(() -> {
synchronized (lockA) { // ✅ 先获取A
synchronized (lockB) { // ✅ 再获取B
// 安全执行
}
}
});自死锁
对于不支持重复加锁的锁,如果线程持有某个锁,而后又再次申请锁,因为该锁已经被自己持有,再次申请锁必然得不到满足,从而导致死锁。