SynchornizedLearnNote

JAVA 八股文 锁相关

锁的分类的知识

  1. 公平锁vs非公平锁(谁先排队谁先上)

这说的是线程排队的态度。

- 公平锁:
    - 规矩:严格遵守先来后到
    - 场景:食堂排队打饭,后来的人必须在队尾,不许插队
    - 优缺点:很文明,不会有人饿死,但频繁唤醒排队的人很累,性能较低

- 非公平锁:
    - 规矩:允许“截胡”
    - 场景:饭店门口有人排队,但正好有个空位出来时,正好刚走过来一个人,他直接冲进去问“有位吗?”,服务员说“有”,他就直接坐下了。排队的人只能继续等
    - 优缺点:性能极高(减少了唤醒排队者的开销),因为刚来的线程处于活跃状态,直接用就行。Java 的 synchronized 和 ReentrantLock 默认都是非公平的
  1. 乐观锁 vs 悲观锁 (你觉得别人会搞破坏吗?)

这说的是对待竞争的心态。

- 悲观锁 (Pessimistic Lock):
    - 心态:总觉得“总有刁民想害朕”。
    - 行为:干活前必须先上锁,谁也别想碰。
    - 例子:synchronized。不管有没有人抢,先锁了再说。

- 乐观锁 (Optimistic Lock):
    - 心态:觉得“大家都很文明,基本没人抢”。
    - 行为:不上锁。直接去改数据,改完提交时检查一下:“咦?刚才我改的时候,别人动过没?” 如果动过,就回滚重试。
    - 例子:CAS 机制(Compare And Swap)。比如数据库里的 version 版本号控制。
  1. 可重入锁

这说的是锁的识别能力。也叫“递归锁”。

- 定义:如果一个线程已经拿到了大门的锁,当它想进屋里的小门时(同一个锁保护的另一个代码块),不需要重新抢锁,直接进去。
- 如果不满足可重入:你在大门口锁了门,进屋后发现小门也锁着,你得把大门的锁放下再去抢小门的锁——结果你放不下大门的锁,自己把自己锁死了。
- 例子:synchronized。
1
2
3
synchronized void 方法A() {
方法B(); // 方法B也是synchronized,同一线程可以直接进,这就是可重入
}
  1. 共享锁 vs 排他锁 (能一起看吗?)

这说的是锁的霸占程度。
- 排他锁 (Exclusive Lock):也叫独占锁。
- 规则:我锁了,谁也别想碰(不能读,不能写)。
- 例子:写操作、synchronized。
- 共享锁 (Shared Lock):
- 规则:大家可以一起看,但谁都不能改。
- 例子:ReentrantReadWriteLock 里的 读锁。100 个线程同时读都没问题,但只要有一个人想写,所有人都得停下。

  1. 总结:

    • 公平/非公平:看是不是按顺序排队。
    • 乐观/悲观:看是先上锁还是先干活。
    • 可重入:看同一个线程能不能“套娃”进锁。
    • 共享/排他:看能不能让多个人同时进。

在 Java 面试里,如果你能说出 “synchronized 是非公平的、悲观的、可重入的、排他的锁”,这一句就把四个概念全串起来了。

synchronized锁进化

偏向锁->轻量级锁->重量级锁

1. 偏向锁:线程A进来后直接在对象头的Makeword进行标记,以后线程A再来看一眼id直接进,无锁开销
2. 轻量级锁:线程B进来后发现对象已经被线程A占用,他在门口自旋并通过CAS去不断尝试将对象头指向自己的栈(线程B在自己的栈帧里开辟的空间lock record),这时候线程B是running状态
3. 重量级锁:当B自旋到极限或同时有C、D或者更多抢锁的人,对象头膨胀创建monitor监视器,将B、C、D全部移入阻塞队列,此时B、C、D状态从running到blocked,等待线程A干完活,会触发信号告诉监视器,唤醒阻塞队列的线程

死锁

  • 定义:两个或多个线程互相持有对方需要的锁,且都不肯放手,导致永久等待。
  • 解决方法:按顺序加锁(大家都先抢 A 再抢 B),或者使用 ReentrantLock.tryLock() 设置超时时间,抢不到就撤。

ReentrantLock八股

ReentrantLock 对比 synchronized

- 实现层面:synchronized是jvm的关键字
- 核心机制:其底层基于AQS(AbstractQueuedSynchronizer)实现,利用一个volatile修饰的state变量来表示锁的状态

代码结构设计 内部类+模版组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Reentrantlock {

private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {

// 父类自己实现一个trylock 供Reentrantlock的无参的trylock调用 这里是非公平的实现
final boolean tryLock() {}

// 然后创建抽象方法 让不同的类实现
abstract boolean initialTryLock();

// 然后lock的时候调用不同的子实现
final void lock() {
if (!initialTryLock())
acquire(1);
}

}

static final class NofairSync extends Sync {

// 不公平的
final boolean initialTryLock() {

}


static final class FairSync extends Sync {}

// 公平的
final boolean initialTryLock() {
}

// 真实调用的时候

// 无参默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}

// 有参根据fair值判断是初始化公平还是非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

// 调用锁的时候 去调用sync即初始化的fairlock或nofairlock的锁
public void lock() {
sync.lock();
}

// 无参的trylock 直接调用sync类的trylock
public boolean tryLock() {
return sync.tryLock();
}

// 有参的trylock调用fairlock或nofairlock的
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryLockNanos(unit.toNanos(timeout));
}
final boolean tryLockNanos(long nanos) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// ||短路写法 先走前面的锁 如果锁到了就不走后面 没锁到就走后面的带超时的锁
return initialTryLock() || tryAcquireNanos(1, nanos);
}

加锁逻辑

加锁的核心逻辑是通过修改 AQS 中的 state 变量(volatile 类型)来实现的:

  • 初次获取:如果 state 为 0,表示锁未被占用。线程通过 CAS 操作将其修改为 1,并设置自己为独占线程。
  • 可重入性:如果 state > 0 且当前线程就是持有锁的线程,则 state 累加(代表重入次数),直接获取成功。
  • 入队阻塞:若获取失败且非重入,线程会被封装成 AQS 节点 加入等待队列,并通过 LockSupport.park() 挂起。