独占锁和共享锁唤醒机制

独占锁和共享锁唤醒机制

精确总结

独占锁(EXCLUSIVE)

// 释放时:unlock() → release() → unparkSuccessor()
// 只唤醒队列中的第一个有效后继节点
// 唤醒后整个释放过程就结束了

特点

  • 一次释放,只唤醒一个线程
  • 被唤醒的线程获取锁后不会主动唤醒其他线程
  • 唤醒链不传播

共享锁(SHARED)

// 释放时:releaseShared() → doReleaseShared()
// 1. 先唤醒第一个后继节点
// 2. 被唤醒的线程在返回前,会调用setHeadAndPropagate()
// 3. setHeadAndPropagate()中会再次调用doReleaseShared()唤醒下一个
// 4. 形成唤醒链,直到没有需要唤醒的节点

特点

  • 一次释放,触发连锁唤醒
  • 被唤醒的线程会"接力"唤醒下一个
  • 唤醒链会一直传播

独占锁的正确顺序:

初始:线程1、2、3都在等待队列中
      head → 线程1 → 线程2 → 线程3 → tail

1. 释放锁 → unparkSuccessor(head) → 唤醒线程1
2. 线程1被唤醒 → 获取锁 → 执行任务
3. 线程1执行完毕 → unlock() → 再次unparkSuccessor() → 唤醒线程2
4. 线程2被唤醒 → 获取锁 → 执行任务
5. 线程2执行完毕 → unlock() → 唤醒线程3
6. 线程3被唤醒 → 获取锁 → 执行任务

关键:执行和唤醒是串行的,一个线程执行完才唤醒下一个

共享锁的正确顺序:

初始:线程1、2、3都在等待队列中  
      head → 线程1 → 线程2 → 线程3 → tail

1. 释放资源 → doReleaseShared() → unparkSuccessor(head) → 唤醒线程1
2. 线程1被唤醒 → 获取共享资源 → 立即调用setHeadAndPropagate()
3. setHeadAndPropagate()中调用doReleaseShared() → 唤醒线程2
4. 线程2被唤醒 → 获取共享资源 → 立即调用setHeadAndPropagate()  
5. setHeadAndPropagate()中调用doReleaseShared() → 唤醒线程3
6. 线程3被唤醒 → 获取共享资源 → ...
7. 所有线程几乎同时开始执行(如果资源足够)

关键:唤醒是链式、立即的,可能所有线程都被唤醒后才开始执行任务

核心区别可视化

独占:唤醒1 → [线程1执行] → 唤醒2 → [线程2执行] → 唤醒3 → [线程3执行]
     ↑                     ↑                     ↑
     执行和唤醒严格交替      执行完才唤醒下一个       串行执行

共享:唤醒1 → 唤醒2 → 唤醒3 → [线程1、2、3几乎同时开始执行]
     ↑        ↑        ↑
     快速链式唤醒      不需要等待前一个执行完      并行/并发执行

独占锁(接力赛)

// 像接力棒,必须一个接一个
┌─────────┐    ┌─────────┐    ┌─────────┐
│ 唤醒线程1 │──→│线程1执行│──→│ 唤醒线程2 │──→│线程2执行│──→│ 唤醒线程3 │
└─────────┘    └─────────┘    └─────────┘    └─────────┘    └─────────┘
     时间:t1         t2            t3            t4            t5

共享锁(广播链)

// 像多米诺骨牌,快速连续触发
┌─────────┐    ┌─────────┐    ┌─────────┐
│ 唤醒线程1 │──→│ 唤醒线程2 │──→│ 唤醒线程3 │
└─────────┘    └─────────┘    └─────────┘
      │             │             │
      ↓             ↓             ↓
┌─────────┐    ┌─────────┐    ┌─────────┐
│线程1执行│    │线程2执行│    │线程3执行│    ← 几乎同时开始
└─────────┘    └─────────┘    └─────────┘
     时间:t2         t2           t2      (时间很接近)

代码验证

public class WakeupTimingDemo {
    static long startTime;
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== 测试独占锁唤醒时序 ===");
        testExclusiveTiming();
        
        Thread.sleep(1000);
        
        System.out.println("\n=== 测试共享锁唤醒时序 ===");
        testSharedTiming();
    }
    
    static void testExclusiveTiming() throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        startTime = System.currentTimeMillis();
        
        // 主线程先获取锁
        lock.lock();
        
        // 创建3个线程
        for (int i = 1; i <= 3; i++) {
            final int id = i;
            new Thread(() -> {
                lock.lock();  // 等待锁
                try {
                    long time = System.currentTimeMillis() - startTime;
                    System.out.printf("  线程%d 在 %dms 后获得锁,执行1秒\n", id, time);
                    Thread.sleep(1000);  // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }).start();
        }
        
        Thread.sleep(100);
        System.out.println("主线程在 100ms 释放锁");
        lock.unlock();  // 释放锁,开始唤醒链
        
        Thread.sleep(5000);
    }
    
    static void testSharedTiming() throws InterruptedException {
        Semaphore semaphore = new Semaphore(0);  // 0个许可,所有线程都会阻塞
        startTime = System.currentTimeMillis();
        
        // 创建3个线程
        for (int i = 1; i <= 3; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    semaphore.acquire();  // 等待许可
                    long time = System.currentTimeMillis() - startTime;
                    System.out.printf("  线程%d 在 %dms 后被唤醒,执行1秒\n", id, time);
                    Thread.sleep(1000);  // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        
        Thread.sleep(100);
        System.out.println("主线程在 100ms 释放1个许可");
        semaphore.release(3);  // 释放3个许可,会链式唤醒
        
        Thread.sleep(5000);
    }
}

可能的输出

=== 测试独占锁唤醒时序 ===
主线程在 100ms 释放锁
  线程1 在 100ms 后获得锁,执行1秒
  线程2 在 1100ms 后获得锁,执行1秒  ← 注意:线程1执行完才唤醒线程2
  线程3 在 2100ms 后获得锁,执行1秒  ← 线程2执行完才唤醒线程3

=== 测试共享锁唤醒时序 ===  
主线程在 100ms 释放1个许可
  线程1 在 100ms 后被唤醒,执行1秒
  线程2 在 101ms 后被唤醒,执行1秒  ← 几乎同时被唤醒!
  线程3 在 102ms 后被唤醒,执行1秒  ← 几乎同时被唤醒!

关键区别表格

对比项 独占锁 共享锁
唤醒时机 前一个线程执行完后才唤醒下一个 立即链式唤醒,不管前一个是否开始执行
执行时机 严格串行,一个接一个 可能并发执行,特别是资源足够时
唤醒链 执行完才传递"唤醒权" 唤醒时就传递"唤醒权"
性能影响 延迟大,吞吐量低 延迟小,吞吐量高

总结

时间顺序:

  1. 独占锁是"执行-唤醒-执行-唤醒"的串行模式
  2. 共享锁是"唤醒-唤醒-唤醒-执行"的快速传播模式

这也是为什么共享锁(如Semaphore)适合高并发场景的原因:它能快速唤醒大量等待线程,提高系统吞吐量。而独占锁(如ReentrantLock)则更注重互斥性和公平性。

posted @ 2025-12-09 20:10  deyang  阅读(5)  评论(0)    收藏  举报