速记

synchronized:可重入锁、非公平锁、独占锁

ReentrantLock :可重入锁、可通过传递参数实现公平和非公平锁、独占锁

  • state0 表示锁空闲,> 0 表示锁被占用,且数值代表当前线程的重入次数

ReentrantReadWriteLock :可重入锁、可通过传递参数实现公平和非公平锁、读锁(共享锁)、写锁(独占锁)

  • 32 位的 state 整数一分为二 ,高 16 位管读锁,低 16 位管写锁

  • 读锁计数(共享锁数量):共享锁-可重入锁

  • 写锁计数(独占锁重入次数):独占锁-可重入锁

                    ┌─────────────────────────┐
                    │       锁的类型           │
                    └─────────────────────────┘
                                │
                ┌───────────────┴───────────────┐
                │                               │
          独占锁(排他)                     共享锁(共享)
    同一时刻只能一个线程持有            同一时刻多个线程可持有
                │                               │
       ┌────────┴────────┐              ┌──────┴──────┐
       │                 │              │             │
  可重入独占锁       不可重入独占锁    可重入共享锁   不可重入共享锁
  (ReentrantLock)  (ThreadPoolExecutor  (ReadLock)    (极少见)
  (synchronized)   的 Worker 锁)

锁(可重入锁)

synchronized:JVM 层面记录锁计数器(monitor 的计数器)

  • 可重入锁、非公平锁

ReentrantLock :state0 表示锁空闲,> 0 表示锁被占用,且数值代表当前线程的重入次数

  • 可重入锁
  • 可通过传递参数实现公平和非公平锁
  • 独占锁

ReentrantReadWriteLock : 32 位的 state 整数一分为二 ,高 16 位管读锁,低 16 位管写锁

  • 可通过传递参数实现公平和非公平锁

  • 读锁计数(共享锁数量):共享锁

  • 写锁计数(独占锁重入次数):独占锁-可重入锁

  • 问题:用读写锁时,我读的时候正在写,怎么保证正确性的???
    解答:读写锁通过加锁时的exclusiveCount(state)检查,保证读操作在写锁释放前绝不会被授予;通过 volatile 的可见性语义,保证写操作的结果对后续读操作立即可见。
    

为什么 99% 的锁设计成“可重入”?

原因 说明
避免死锁 如果一个同步方法调用另一个同步方法(同一个锁),不可重入就会导致死锁
更符合直觉 程序员通常认为“我已经拥有这把锁,再次进入应该没问题”
简化编程 不用在每个递归或嵌套调用中反复判断是否已持有锁

锁(不可重入)

1. 最经典的不可重入锁:ThreadPoolExecutor 内部的 ReentrantLock 变种

ThreadPoolExecutor 中,有一个内部类 Worker,它继承自 AbstractQueuedSynchronizer实现了一个不可重入的独占锁

// ThreadPoolExecutor.Worker 内部类(简化)
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    // 尝试获取锁:如果 state == 0 则 CAS 获取,否则失败
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false; // ★ 如果已经持有,直接返回 false,不允许重入
    }

    // 释放锁:只有持有者才能释放
    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    // 外部调用的 lock 方法
    public void lock() {
        acquire(1);
    }
}

为什么 Worker 要设计成不可重入?

  • Worker 代表一个工作线程,它在执行任务前会 lock()
  • 如果任务中又调用了 execute()submit() 提交新任务,并尝试获取同一个 Worker 的锁,一旦可重入,就可能导致任务循环依赖资源耗尽
  • 设计成不可重入,可以快速暴露这种错误逻辑(第二次获取直接失败),而不是让线程死锁或无限等待。

2. 手动实现不可重入锁(基于 AQS)

你可以轻松自己实现一个不可重入锁:

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class NonReentrantLock {
    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int acquires) {
            // ★ 如果 state == 0,则获取锁;如果 > 0,直接失败
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false; // 不允许重入
        }

        @Override
        protected boolean tryRelease(int releases) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
}

测试一下:

NonReentrantLock lock = new NonReentrantLock();

lock.lock();
System.out.println("第一次获取成功");
lock.lock(); // ★ 这里会阻塞,因为 state == 1,tryAcquire 失败!
System.out.println("第二次获取成功(不会执行到这里)");
posted @ 2026-07-04 15:52  deyang  阅读(6)  评论(0)    收藏  举报