可重入锁 ReentrantLock

前文讲解的以关键字 synchronized 为代表的悲观锁,和以并发原子类为代表的乐观锁都可以保证对共享资源修改时(或访问时)的排他性和修改后的可见性。

其实和并发原子类一起在 Java1.5 版本引入的还有一个显式锁 Lock,二者都是对 关键字 synchronized 的补充,并发原子类存在的目的是针对竞争不激烈 的情况下对单一变量操作 提供的一种无锁 的安全操作,性能比synchronized 更高,而今天介绍的显式锁 Lock 比起 synchronized 又有什么优势呢?

基本使用

JDK提供的显式锁都在 java.util.concurrent.locks 包下,大致UML类图如下:

今天先以 ReentrantLock 为例,讲解 Lock 锁,至于读写锁 ReadWriteLock 放到下一篇。

Lock 接口

Lock 接口是 Java 并发包(java.util.concurrent.locks)中的核心接口,定义了一组用于锁机制的通用方法,提供比 synchronized 更灵活的锁管理方式。

void lock() 请求获取锁。如果锁已经被其他线程持有,调用线程将阻塞直到锁可用。等同于 synchronized 的作用,但更加灵活,不会自动释放锁,必须显式调用 unlock() 来释放锁。

java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();  // 必须在 finally 中确保释放锁
}

void lockInterruptibly() throws InterruptedException 请求获取锁,但可以响应中断。如果线程在等待获取锁时被中断,则会抛出 InterruptedException

java 复制代码
Lock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();  // 响应中断的锁请求
    // 临界区代码
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt();
} finally {
    lock.unlock();
}

boolean tryLock() 尝试获取锁。如果锁可用,则立即获取锁并返回 true,如果锁不可用,则立即返回 false,不会阻塞线程。

java 复制代码
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 锁不可用时执行其他操作
}

boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在指定的时间范围内尝试获取锁。如果锁在给定的超时时间内可用,则获取锁并返回 true,否则在超时后返回 false。如果在等待期间线程被中断,则抛出 InterruptedException

void unlock() 释放锁。调用此方法的线程必须是持有该锁的线程,否则会抛出 IllegalMonitorStateException

可以看到,Lock 接口提供了四种不同获取锁的方式,各有特色:

方法 功能 描述
lock() 阻塞获取锁 等同于 synchronized 阻塞获取锁的方式
lockInterruptibly() 阻塞获取锁但允许中断 适用于可以中断的场景,允许线程在等待锁的过程中响应外部中断信号。
tryLock() 非阻塞锁获取 类似于原子类的 CAS 操作,适用于不想让线程无限期等待获取锁的场景,允许线程检测锁是否可用而不阻塞
tryLock(long time, TimeUnit unit) tryLock()lockInterruptibly() 的结合体 适用于有时间限制的锁请求场景,允许线程在有限的时间内尝试获取锁,并能响应中断。

除了加锁与解锁最基本的操作,还有一个用于获取 绑定到该锁的 Condition 实例的方法,这个Condition 可以类比 synchronized 关键字的条件等待队列。

Condition newCondition(); 返回一个绑定到该锁的 Condition 实例,用于线程之间的协调(等待与通知机制)。

Condition 接口

Condition 对象通常与锁(Lock)配合使用,用于控制线程的等待和唤醒。类似于 Object 类中的 wait()notify(),但功能更强大。

java 复制代码
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    condition.await();  // 当前线程进入等待状态
    // 被唤醒后执行后续操作
} finally {
    lock.unlock();
}

void await() throws InterruptedException 使当前线程等待,直到被其他线程 signal()signalAll() 唤醒,或线程被中断。线程在等待时必须先获取与该 Condition 关联的锁。

boolean await(long time, TimeUnit unit) throws InterruptedException 使当前线程等待指定的时间,直到被唤醒(通过 signal()signalAll())、超时、或线程被中断。返回一个布尔值,表示是否在等待时间内被唤醒(true 表示正常唤醒,false 表示超时)。

boolean awaitUntil(Date deadline) throws InterruptedException 使当前线程等待直到某个时间点 deadline,如果线程在这个时间之前被 signal()signalAll() 唤醒,返回 true,如果超时到达 deadline 则返回 false

long awaitNanos(long nanosTimeout) throws InterruptedException 使当前线程等待给定的纳秒数,直到被 signal()signalAll() 唤醒,或超时,或线程被中断。返回剩余的等待时间,如果超时则返回一个负值。

void awaitUninterruptibly() 使当前线程等待,直到被其他线程 signal()signalAll() 唤醒,但不同于 await(),线程在等待时不会响应中断,即中断发生时线程不会抛出 InterruptedException,而是继续等待。

void signal() 唤醒一个等待在 Condition 上的线程。被唤醒的线程必须重新获得锁才能继续执行。signal() 只会唤醒一个线程,而不是所有等待的线程。

void signalAll() 唤醒所有等待在 Condition 上的线程。被唤醒的线程必须重新获得锁才能继续执行。与 signal() 不同,signalAll() 会唤醒所有等待的线程。

看起来, Condition 提供了更加灵活的线程间通信机制,但说到底也就是 wait()notify() 嘛。

ReentrantLock

ReentrantLock 是 JUC 提供的显式锁,相比 synchronized 关键字,它提供了更多的功能:

▪ 可重入

可重入锁,顾名思义就是具有可重入性,即同一个线程可以多次获取同一把锁而不会发生死锁。其实 synchronized 关键字就是可重入锁,锁的重入次数记录在 ObjectMonitor 的 _recursions 属性中,那 ReentrantLock 的重入次数记录在哪呢?下面讲解原理的时候再聊。

▪ 公平锁与非公平锁

ReentrantLock 可以通过构造方法设置为公平锁或者非公平锁,默认是非公平锁。

ReentrantLock():创建一个非公平锁(默认)。

ReentrantLock(boolean fair):通过传递 true 创建一个公平锁,false 则创建一个非公平锁。

公平锁,顾名思义,先到先得,会按照线程请求锁的顺序来唤醒等待中的线程,避免线程饥饿。而非公平锁则并不保证线程获取锁的顺序, synchronized 就是个非公平锁,当一个线程释放锁时,等待的线程会被唤醒,因为不保证公平,所以某些线程可能会插队,从而导致其他线程长时间得不到锁(线程饥饿问题)。

▪ 对Lock接口的实现

ReentrantLock 作为 Lock接口的实现类,Lock接口定义的锁的基本功能ReentrantLock都可以实现,什么阻塞获取锁,非阻塞获取锁,获取锁的时候可以中断,超时获取锁...

▪ 条件变量 Condition

ReentrantLock 可以创建多个 Condition,也就是说比起 synchronized 只有一个等待队列,可重入锁可以按需创建多个条件队列,更精细化了。

除此之外,ReentrantLock 还提供了一下与锁相关的方法,比如 getHoldCount() 获取当前线程持有锁的次数,isHeldByCurrentThread() 判断当前线程是否持有锁等。

最佳实践

ReentrantLock 玩得好,替代synchronized 绰绰有余,但因为使用起来更复杂,还是需要注意一下的。

总是调用 unlock()

synchronized 自动解锁不同,ReentrantLock 需要显式调用 unlock() 来释放锁,因此,建议将 lock()unlock() 放在 try-finally 代码块中,以确保即使发生异常,锁也能被正确释放。

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();  // 获取锁
try {
    // 执行需要同步的代码
} finally {
    lock.unlock();  // 确保锁被释放
}

而且需要注意的是,lock() 后的代码必须是 try-finally 代码块, unlock() 要放到 finally 代码块第一行。

至于为啥这样?可以考虑一下各个场景:

⨳ 如果lock() 后的代码不是try-finally 代码块:

js 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();  // 获取锁
// 代码①
try {
    // 执行需要同步的代码
} finally {
    lock.unlock();  // 确保锁被释放
}

那如果lock()后的代码①出现异常,就没有解锁的可能了。

⨳ 如果 lock() 的代码放到 try 代码块,比如try 代码块的第一行:

java 复制代码
ReentrantLock lock = new ReentrantLock();
try {
    lock.lock();  // 获取锁
    // 代码①
} finally {
    lock.unlock();  // 确保锁被释放
}

lock() 的代码放到 try 代码块第一行的前提是假设 lock 方法本身不会发生异常,但事实是,lock 方法也会出现运行时异常,虽然概率很低。

那可能你会说,如果我想用 lockInterruptibly 可中断获取锁,那 lockInterruptibly 需要放到 try 代码块中捕获 InterruptedException 异常进行后续处理,那放到try 代码块行不行?

也没事,解锁的时候加一个判断即可:

java 复制代码
Lock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();  // 响应中断的锁请求
    // 临界区代码
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt();
} finally {
    if(lock.isHeldByCurrentThread()){
        lock.unlock();
    }
}

当然也可以搞两个try-finally 代码块:

java 复制代码
Lock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();  // 响应中断的锁请求
    try{
     // 临界区代码
    }finally { 
        lock.unlock(); // 确保在任务完成后释放锁 
        System.out.println(Thread.currentThread().getName() + ": Lock released."); 
    }
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt();
}  

无论如何,只有保证未加的锁的线程不会执行 unlock,已加锁的线程一定能执行 unlock 就可以。

死锁 Deadlock

死锁(Deadlock)是指两个或多个线程在执行过程中相互等待对方持有的锁而无法继续执行,从而导致所有相关线程都无法推进的情况。

java 复制代码
public class DeadlockDemo {

    // 创建两个锁对象
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    // 线程 A 先获取 lock1,然后尝试获取 lock2
    public void methodA() {
        synchronized (lock1) {
            System.out.println("Thread A: Holding lock1...");

            try { Thread.sleep(100); } catch (InterruptedException e) {}

            System.out.println("Thread A: Waiting for lock2...");
            synchronized (lock2) {
                System.out.println("Thread A: Holding lock1 & lock2...");
            }
        }
    }

    // 线程 B 先获取 lock2,然后尝试获取 lock1
    public void methodB() {
        synchronized (lock2) {
            System.out.println("Thread B: Holding lock2...");

            try { Thread.sleep(100); } catch (InterruptedException e) {}

            System.out.println("Thread B: Waiting for lock1...");
            synchronized (lock1) {
                System.out.println("Thread B: Holding lock2 & lock1...");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockDemo demo = new DeadlockDemo();

        // 创建两个线程,分别调用 methodA 和 methodB
        Thread threadA = new Thread(() -> demo.methodA());
        Thread threadB = new Thread(() -> demo.methodB());

        threadA.start();
        threadB.start();
    }
}

上述代码很好理解,线程 A 先持有 lock1,然后在等待 lock2 时休眠100毫秒;线程 B 先持有 lock2,然后在等待 lock1 时休眠100毫秒。由于线程 A 持有 lock1,线程 B 持有 lock2,并且双方都在等待对方释放的锁,最终这两个线程相互等待,导致死锁。

死锁问题,synchronized 关键字也有,但合理使用 ReentrantLock,可以有效避免死锁,为什么呢?这就要谈到造成死锁的四个必要条件了:

互斥条件(Mutual Exclusion) :一个共享资源在同一时刻只能被一个线程独占使用,其他线程在同一时刻不能访问这些资源。比如说线程A 获取到了 lock1 ,线程B 就不能获取到 lock1 了。

持有并等待条件(Hold and Wait) :一个线程在持有至少一个资源的情况下,仍然请求其他资源而没有释放它已持有的资源。比如线程 A 已经持有 lock1,但需要请求 lock2的;线程 B 已经持有 lock2,但还需需要请求 lock1

不剥夺条件(No Preemption) :锁不能被强制释放。比如线程A 必须自己释放 lock1,线程B 不能强制让 线程A 释放 lock1

循环等待条件(Circular Wait) :在一组线程中,每个线程都在等待下一个线程持有的资源,形成一个循环依赖关系。比如线程A 在等待 lock2,线程B 在等待 lock1,形成循环等待。

这四个条件破坏任意一个条件都可以避免死锁,我们一个个来破坏:

▪ 互斥条件(Mutual Exclusion)

我们用锁的目的就是保证互斥,至少是修改共享变量时的互斥,这个条件没有办法破坏。

其实读写锁的读锁不是互斥的,这一点放到下一篇讲解。

▪ 持有并等待条件(Hold and Wait)

持有并等待条件说的是某个线程为了完成一个目的,需要多把锁(如两把锁),在已经获取到一个锁的同时,需要等待另一把锁,如果获取锁的时候不等待不就是将这个条件破坏了嘛。

ReentrantLock 不就有一个非阻塞获取锁的方法------tryLock 吗。

java 复制代码
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

public void safeMethod() {
    boolean gotLock1 = lock1.tryLock();
    boolean gotLock2 = false;
    try {
        if (gotLock1) {
            gotLock2 = lock2.tryLock();
            if (gotLock2) {
                // 临界区代码,获取到所有锁
            }
        }
    } finally {
        if (gotLock2) {
            lock2.unlock();
        }
        if (gotLock1) {
            lock1.unlock();
        }
    }
}

那更进一步,如果我们能一次性获取两把锁(一次性请求所有资源),那也没有持有并等待条件了:

java 复制代码
// 尝试获取两个锁,成功返回 true,失败返回 false 
boolean tryLockBoth(Lock lock1, Lock lock2) { 
    boolean gotLock1 = lock1.tryLock(); 
    boolean gotLock2 = false; 
    if (gotLock1) { 
        gotLock2 = lock2.tryLock(); 
        if (!gotLock2) { 
            // 如果未能获取 lock2,释放 lock1 
            lock1.unlock(); 
        } 
     } 
     return gotLock1 && gotLock2; 
}

▪ 不剥夺条件(No Preemption)

这个条件更好破坏,设置超时锁请求 tryLock(timeout) 或者使用可中断的锁请求 lockInterruptibly() 即可。这个例子不好列举,就不写代码了,意思一下就行。

▪ 循环等待条件(Circular Wait)

这个条件,可以通过规定多个线程获取锁的顺序,比如按照锁对象的固定顺序来获取资源,确保线程获取锁的顺序一致,从而避免循环依赖关系。

java 复制代码
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

public void safeMethod() {
    // 假设我们总是先获取 lock1,再获取 lock2
    lock1.lock();
    try {
        lock2.lock();
        try {
            // 临界区代码
        } finally {
            lock2.unlock();
        }
    } finally {
        lock1.unlock();
    }
}

破坏这个条件最简单,就算使用 synchronized 关键字加锁也能保证获取锁的顺序。

生产者消费者模式

关于生产者消费者模式,前文已经聊的很详细了,最后是使用一个 Channel ,管理者 Data 的存取。

java 复制代码
public class Channel {

    // Data 集合
    private final int MAX_CAPACITY = 10;
    private List<Data> container = new ArrayList<>(MAX_CAPACITY);

    // 将 Data 放入集合
    public synchronized void put(Data data) throws InterruptedException {
        while(container.size() ==MAX_CAPACITY){ // 集合已满
            wait();
        }
        container.add(container.size(),data);
        System.out.println("生产者["+Thread.currentThread().getName()+"] 生产了:"+data);
        notifyAll();
    }


    // 从集合中 取Data
    public synchronized Data take() throws InterruptedException {
        while(container.isEmpty()){    // 集合已空
            wait();
        }
        Data data = container.remove(container.size()-1);
        System.out.println("消费者["+Thread.currentThread().getName()+"] 消费到了:"+data);
        notifyAll();
        return data;
     }

}

前文是使用关键字 synchronized 实现的,虽然能够保证生产者与消费者的同步和通信,但因为关键字 synchronized只有一个条件等待队列,等待"集合未空"条件的消费者,和等待"集合未满"条件的生产者都在一个等待队列里,如果生产者生产了数据,会唤醒所有等待的线程,当然也包括生产者,如果被唤醒的生产者发现集合满了,又得回去睡觉,这不白白浪费了系统资源嘛(惊群效应)。

使用 ReentrantLockCondition,可以创建多个条件变量,并精确控制哪些线程需要等待,哪些线程可以被唤醒,就可以有效避免惊群效应。

如生产者到"集合未满"的条件队列中等待,消费者到"集合未空"的条件队列中等待,生产者生产完数据就唤醒消费者,消费者消费完数据就唤醒生产者,避免了无效的资源竞争。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Channel {

    // Data 集合
    private final int MAX_CAPACITY = 10;
    private final List<Data> container = new ArrayList<>(MAX_CAPACITY);

    // ReentrantLock 和两个 Condition,分别用于生产者和消费者
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    // 将 Data 放入集合
    public void put(Data data) throws InterruptedException {
        lock.lock(); // 获取锁
        try {
            while (container.size() == MAX_CAPACITY) { // 集合已满,等待
                System.out.println("生产者[" + Thread.currentThread().getName() + "] 等待,容器满了...");
                notFull.await(); // 生产者等待
            }
            container.add(data);
            System.out.println("生产者[" + Thread.currentThread().getName() + "] 生产了:" + data);
            notEmpty.signalAll(); // 通知消费者可以取数据了
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 从集合中取 Data
    public Data take() throws InterruptedException {
        lock.lock(); // 获取锁
        try {
            while (container.isEmpty()) { // 集合已空,等待
                System.out.println("消费者[" + Thread.currentThread().getName() + "] 等待,容器空了...");
                notEmpty.await(); // 消费者等待
            }
            Data data = container.remove(container.size() - 1);
            System.out.println("消费者[" + Thread.currentThread().getName() + "] 消费到了:" + data);
            notFull.signalAll(); // 通知生产者可以继续生产了
            return data;
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

美滋滋。

下面聊一下可重入锁的实现原理,AQS。

AQS 抽象队列同步器

AQSAbstractQueuedSynchronizer)是 Java 并发包一个很重要的队列同步器,很多并发工具类(如 CountDownLatchSemaphore...)都是通过它完成线程同步与通信的。

可重入锁 ReentrantLock 也不例外,通过上述 UML 类图可以看到,ReentrantLock 的三个内部类就是对AQS 的个性化扩展。

AQS 的核心组成

AbstractQueuedSynchronizer 核心属性如下:

java 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;

    // Unsafe
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long STATE
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "state");
    private static final long HEAD
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "head");
    private static final long TAIL
        = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "tail");

有没有很熟悉?!这不就是原子类和套路(CAS+volatile)嘛,其中 state 变量表示同步状态,在 ReentrantLock 中 state = 1 表示锁已被某个线程持有,state = 0 表示锁空闲。

除了 state 之外,还有表示队列头尾指针的两个变量: headtail,这也很容易理解吧,AQS 毕竟是个队列,用链表实现很合理呀。这个队列事实上类似于 synchronized 关键字的 _EntryList ,存放着竞争锁失败的线程。

java 复制代码
abstract static class Node {
    volatile Node prev;       // initially attached via casTail
    volatile Node next;       // visibly nonnull when signallable
    Thread waiter;            // visibly nonnull when enqueued
    volatile int status;      // written by owner, atomic bit ops by others
    ...

根据AQS的属性,对比 synchronized 加锁过程,很自然的就能想到 ReentrantLock 的加锁过程:

  1. synchronized 关键字加锁时会先使用 CAS 操作尝试修改 Mark Word 的相关锁信息,那同样的,ReentrantLock 加锁时也会使用 CAS 尝试修改 AQS 的 state 属性(从 0 修改为 1),修改成功则加锁成功,修改失败则加锁失败。

  2. synchronized 关键字加锁失败的线程会被加入到 ObjectMonitor_entryList 中,那同样的,ReentrantLock 加锁失败的线程也会进入的 AQS 维护的双向链表队列中。

  3. synchronized 关键字进入到 _entryList 中的线程会进入阻塞状态,放弃 CPU 的使用权,等待被唤醒,那同样的,ReentrantLock 加锁失败的线程加入到AQS队列后也应该被挂起,等待锁的释放。

下面就结合代码,详细看看这个过程。

无竞争的加锁过程 tryAcquire

js 复制代码
public class ReentrantLock implements Lock, Serializable {
    public void lock() {
        this.sync.lock();
    }

 abstract static class Sync extends AbstractQueuedSynchronizer {
 	 final void lock() {
            if (!this.initialTryLock()) {
                this.acquire(1);
            }
        }
 }

  static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final boolean initialTryLock() {
            Thread current = Thread.currentThread();
            if (compareAndSetState(0, 1)) { // first attempt is unguarded
                setExclusiveOwnerThread(current);
                return true;
            } else if (getExclusiveOwnerThread() == current) {
                int c = getState() + 1;
                if (c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            } else
                return false;
        }

        /**
         * Acquire for non-reentrant cases after initialTryLock prescreen
         */
        protected final boolean tryAcquire(int acquires) {
            if (getState() == 0 && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
    }

}

通过代码可知,ReentrantLock 的 加锁 lock 方法其实是交给 AQS 具体实现类(NonfairSync/FairSync)完成的,实现 AQS 的抽象类 Sync 则是定义了加锁的抽象骨架。

js 复制代码
 abstract static class Sync extends AbstractQueuedSynchronizer {
 	 final void lock() {
            if (!this.initialTryLock()) {
                this.acquire(1);
            }
        }

Sync 加锁的第一步是先尝试加锁 initialTryLock ,NonfairSync.initialTryLock() 有三个 if 分支:

  1. compareAndSetState(0, 1): state == 0 时直接 CAS 抢锁

抢锁成功,就通过 setExclusiveOwnerThread 方法,把当前线程记录到 AQS 的抽象父类 AbstractOwnableSynchronizer 中:

js 复制代码
public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    private transient Thread exclusiveOwnerThread;
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
}
  1. getExclusiveOwnerThread() == current:当前线程已经持有锁时,直接重入。

重入的意思是 state + 1,对于可重入锁来说,state 记录的就是锁的获取次数,即当前线程持有该锁的次数,对于其他AQS的实现类这个 state 有别的含义。

  1. 其他情况, acquire(1),交给 AQS。

这是不是有点像 synchronized 的轻量级锁,都是直接就 CAS 了,不需要阻塞队列。

存在竞争的加锁过程 acquire

当存在线程竞争时,就需要 AQS 处理加锁过程了。

js 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {  

    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;

  public final void acquire(int arg) {
        if (!tryAcquire(arg))
            acquire(null, arg, false, false, false, 0L);
    }

  

AQS 在 acquire 获取锁之前,再给子类一个尝试CAS获取锁的机会,如果还不行就真正的进入 acquire 的多参数重载方法。

js 复制代码
    /**
     * AQS 核心获取方法(JDK 21+ 重构版),把入队、自旋、阻塞、唤醒、超时、中断
     * 全部收拢到一个循环里,替代了老版本的 addWaiter + acquireQueued。
     *
     * @param node          当前线程对应的节点,首次进入为 null
     * @param arg           获取参数,通常传 1
     * @param shared        是否共享模式
     * @param interruptible 是否响应中断
     * @param timed         是否带超时
     * @param time          超时截止时间(纳秒)
     * @return 1 表示获取成功,0 表示被取消/中断
     */
    final int acquire(Node node, int arg, boolean shared,
                      boolean interruptible, boolean timed, long time) {
        Thread current = Thread.currentThread();
        byte spins = 0, postSpins = 0;   // 自旋计数,唤醒后重试时递减
        boolean interrupted = false, first = false;
        Node pred = null;                // 当前节点的前驱

        for (;;) {
            // ====== 阶段1:检查前驱,判断是否已排到队头 ======
            if (!first && (pred = (node == null) ? null : node.prev) != null &&
                !(first = (head == pred))) {
                // 前驱已被取消(status < 0),清理队列后重试
                if (pred.status < 0) {
                    cleanQueue();
                    continue;
                }
                // 前驱的 prev == null,说明前驱刚成为 head 但 next 还没连上,
                // 处于入队序列化过程中,自旋等待
                else if (pred.prev == null) {
                    Thread.onSpinWait();
                    continue;
                }
            }

            // ====== 阶段2:已排到队头(或队列为空),尝试获取 ======
            if (first || pred == null) {
                boolean acquired;
                try {
                    if (shared)
                        acquired = (tryAcquireShared(arg) >= 0);
                    else
                        acquired = tryAcquire(arg);
                } catch (Throwable ex) {
                    // 获取异常,取消本次获取并抛出
                    cancelAcquire(node, interrupted, false);
                    throw ex;
                }
                if (acquired) {
                    if (first) {
                        // 获取成功:当前节点成为新 head,断开旧连接
                        node.prev = null;
                        head = node;
                        pred.next = null;
                        node.waiter = null;
                        // 共享模式下可能需要继续唤醒后继
                        if (shared)
                            signalNextIfShared(node);
                        // 恢复中断状态
                        if (interrupted)
                            current.interrupt();
                    }
                    return 1;
                }
            }

            // ====== 阶段3:队列尚未初始化,先创建头节点 ======
            Node t;
            if ((t = tail) == null) {
                if (tryInitializeHead() == null)
                    return acquireOnOOME(shared, arg);
            }
            // ====== 阶段4:节点尚未创建,先 new 出来(入队前再试一次) ======
            else if (node == null) {
                try {
                    node = shared ? new SharedNode() : new ExclusiveNode();
                } catch (OutOfMemoryError oome) {
                    return acquireOnOOME(shared, arg);
                }
            }
            // ====== 阶段5:CAS 入队,挂到 tail 后面 ======
            else if (pred == null) {
                node.waiter = current;
                node.setPrevRelaxed(t);       // 用 relaxed 写,避免多余屏障
                if (!casTail(t, node))
                    node.setPrevRelaxed(null); // CAS 失败,回退 prev
                else
                    t.next = node;            // 连接前驱的 next
            }
            // ====== 阶段6:排到队头后的自旋重试,减少 unfairness ======
            else if (first && spins != 0) {
                --spins;
                Thread.onSpinWait();
            }
            // ====== 阶段7:设置 WAITING 状态,准备被 signal ======
            else if (node.status == 0) {
                node.status = WAITING;
            }
            // ====== 阶段8:park 阻塞,等待释放线程 unpark ======
            else {
                long nanos;
                spins = postSpins = (byte) ((postSpins << 1) | 1);
                if (!timed)
                    LockSupport.park(this);
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;                      // 超时,跳出循环
                node.clearStatus();             // 清除状态,准备重试
                if ((interrupted |= Thread.interrupted()) && interruptible)
                    break;                      // 被中断且可中断,跳出
            }
        }
        // 跳出循环:超时或被中断,取消获取
        return cancelAcquire(node, interrupted, interruptible);
    }

核心就是创建一个线程 Node(ExclusiveNode/SharedNode),CAS 入队,挂到 AQS 的阻塞链表 tail 后面,然后设置 WAITING 状态,最后调用 LockSupport 的 park 方法进行阻塞,等待唤醒。

解锁过程 release

js 复制代码
// ReentrantLock
public void unlock() {
   sync.release(1);
}

// Sync    
 protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (getExclusiveOwnerThread() != Thread.currentThread())
        throw new IllegalMonitorStateException();
    boolean free = (c == 0);
    if (free)
        setExclusiveOwnerThread(null);
    setState(c);
    return free;
}   

// AQS
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        signalNext(head);
        return true;
    }
    return false;
}

private static void signalNext(Node h) {
    Node s;
    if (h != null && (s = h.next) != null && s.status != 0) {
        s.getAndUnsetStatus(WAITING);
        LockSupport.unpark(s.waiter);
    }
}

解锁过程还是挺清晰的,就是释放锁的时候,把记录重入次数的 state -1,当state 减到 0,清空 AbstractOwnableSynchronizer 中记录的当前线程,再拿到阻塞排队的第一个线程节点 h.next,将给节点的 WAITING 状态清空,最终调用 LockSupport 的 unpark 方法,唤醒该线程。

注意这个解锁过程没有把唤醒的线程节点从链表中删除的逻辑,JDK 21+ 把逻辑一块放进了加锁的 acquire 循环里,因为 AQS 唤醒线程都是从 head 开始唤醒,这样其他线程加锁时就可以直接把 head 指针向后移动一位即可,所以 AQS 的出队确实不是"删节点",而是"推进 head"。

这一点就和 synchronized 不一样了,synchronized 的阻塞队列并不会按 FIFO 顺序唤醒,因为 AQS 可以保证阻塞队列可以按FIFO 顺序唤醒,这也让公平锁有了实现的可能。

公平锁 FairSync

通过 UML 类图可以看到,ReentrantLock 内部有两个继承 AQS 的两个具体实现类,一个是 公平锁 FairSync 和 一个是 非公平锁 NonfairSync

公平 的意思是要保证先到先得,只有阻塞队列 _EntryList 里没有比新到的线性更早等待的线程,你才能去抢锁。

js 复制代码
protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && !hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

公平就不讲武德了,管你有没有先到的线程,直接 CAS 抢锁。

js 复制代码
protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

可以看到,公平锁 在尝试加锁的时候比 非公平锁 多了一步 hasQueuedPredecessors 校验,检查队列里有没有人排前面,有就老实排队。

ReentrantLock 默认是 非公平锁,因为非公平锁可以减少线程上下文切换,正在执行的线程 CAS 抢到锁了,接着执行就好了,完全不需要唤醒阻塞的线程,再切换,再执行。

条件变量 Condition

前文讲过 Condition 类似 Object 类中的 wait()notify(),下面就看看 Condition 是怎么实现线程的等待与唤醒的。

当 ReentrantLock 调用 newCondition 方法时,最终会创建 AQS 的内部类 ConditionObject。

js 复制代码
public class ConditionObject implements Condition, java.io.Serializable {
    /** 条件队列头节点 */
    private transient ConditionNode firstWaiter;
    /** 条件队列尾节点 */
    private transient ConditionNode lastWaiter;

看 ConditionObject 成员属性就知道,等待队列和 AQS 的阻塞队列差不多,也是通过链表实现的。

简单来说,让线程等待的 await 方法大致就是创建一个条件等待节点,把节点挂到条件队列尾部,并把当前线程持有的锁完全释放

唤醒线程的 signal 方法就是从头节点取一个线程节点,把它放到 AQS 的阻塞队列尾部,如果阻塞队列的前驱节点状态异常,就立即调用 LockSupport 的 unpark 方法进行唤醒,否则由前驱节点在后续释放锁或取消时负责唤醒它。

总结

从上可知,显式锁 Lock 提供更丰富的功能,使开发者在处理并发编程时能够更加灵活、高效地控制线程同步。

更好的性能控制 :显式锁 Lock 提供了更多的手动控制,特别是在复杂场景中,可以根据具体需求采取不同的锁策略,例如可以选择公平锁或非公平锁来决定线程获取锁的顺序;例如可以尝试在一定时间内获取锁,而不必像 synchronized 一样必须等待锁释放,...。

多条件变量支持 :一个 ReentrantLock 可以创建多个 Condition 实例,每个 Condition 维护独立的条件队列,能够精确唤醒特定条件的等待线程;而 synchronized 配合 wait/notify 只有一个等待集,notify 只能随机唤醒一个,notifyAll 则会唤醒全部,粒度粗糙且容易产生无效唤醒。

⨳ ...

汇总一下 synchronized、原子类型(如 AtomicInteger)和显式锁 Lock ,它们都是用于控制多线程访问共享资源的机制。

各有优缺点,适用于不同的场景:

特性 synchronized 原子类型(如 AtomicInteger 显式锁 Lock
性能 JDK1.6后性能优化好,但高竞争下可能慢 性能优于锁(低竞争下) 高度可控,性能在复杂场景中优于 synchronized
使用复杂度 简单,自动释放锁 简单,针对单一变量操作 需要手动控制,容易出错
灵活性 较低,阻塞式,不可中断 只适合简单操作 高,可中断、支持条件等待、超时等
死锁风险 较低 存在,必须手动释放锁
场景适用性 简单同步 单一变量更新或简单引用更新 复杂同步或高并发场景
相关推荐
IT枫斗者枫哥1 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java
devpotato1 小时前
HashMap 扩容机制:从源码细节到工程实践
java
yueping21 小时前
如何用idea打开jar包
java
IT_Octopus1 小时前
从一个应用开发者的角度,搞懂大数据查询的完整链路
java·大数据·数据库
许彰午1 小时前
51-BpmnDesigner集成
java·低代码·架构
蜗牛互联网1 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端
泡海椒2 小时前
评分系统最佳实践:JQuick-Java实现权重、阈值动态配置评分
java·人工智能·python
三克的油2 小时前
java-学习1
java·开发语言·学习
mldong2 小时前
弃用 ZCode,转 DeepSeek Harness:我用 GitHub Actions 自建 Windows 打包的全实录
java·架构