Java 队列同步器 与 Lock锁详解

1. 什么是Lock接口

前面介绍了synchronized锁,我们知道其完全是在JVM层面实现的,这里介绍的Lock接口及其实现类是Java语言层面通过CAS机制实现的,相对来说更加灵活,我们甚至可以自定义锁。Lock接口的实现类有很多,我们挑一些常用的介绍下。如下所示:

  • ReentrantLock:用途最广泛,支持可重入(一个线程可以多次获取同一把锁,这个和其实现原理有关,同步状态支持累加),同时支持公平锁和非公平锁(公平锁指的是线程按照FIFO的顺序去获取锁)
  • ReentrantReadWriteLock:读写锁,这里其实包含了两个锁,ReadLock和WriteLock,ReadLock是支持多个线程并发的,WriteLock只支持独占,读和写是互斥的。其也支持公平锁和非公平锁。
  • StampedLock:不支持重入,这里也包含两个锁:读锁和写锁。支持并发读,读锁和写锁是互斥的。StampedLock引入了版本号的机制,类似CAS,使其可以支持乐观读(先不加读锁,发现冲突后,再加读锁进行读,)

1.1. ReentrantLock示例

Java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDeomo01 {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock reentrantLock = new ReentrantLock();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(()->{
            try {
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " into sync code block");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                System.out.println(Thread.currentThread().getName() + " release lock");
                reentrantLock.unlock();
            }
        });

        TimeUnit.MICROSECONDS.sleep(100);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(()->{

            try {
                System.out.println(Thread.currentThread().getName() + " wait reentrantLock");
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " get reentrantLock after about 1 second");
            }
            finally {
                reentrantLock.unlock();
            }
        });

        t1.join();
        t2.join();
    }
}

这里线程t1获取到锁后,睡眠1秒钟,线程t2被阻塞在那里,直到线程t1释放了锁。从上面代码可以看出,线程在进入和退出同步块的时候,都是显示指定的。这种显示指定有一个好处,不同的锁的获取和释放可以交错进行,synchronized很难做到这一点,像下面这样:

获取LockA --> 获取LockB --> 释放LockA --> 释放LockB

1.2. ReentrantReadWriteLock示例

ReentrantReadWriteLock中包含了两个锁:一个读锁和一个写锁,从下面代码的输出结果可以看出,读锁是支持被多个线程同时获取的。

Java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDeomo01 {

    public static void main(String[] args) throws InterruptedException {

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(()->{
            try {
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + " into sync code block");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                System.out.println(Thread.currentThread().getName() + " release lock");
                readLock.unlock();
            }
        });

        TimeUnit.MICROSECONDS.sleep(100);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(()->{

            try {
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + " into sync code block");
            }
            finally {
                readLock.unlock();
            }
        });

        t1.join();
        t2.join();
    }
}

输出结果:

csharp 复制代码
t1_thread into sync code block
t2_thread into sync code block
t1_thread release lock

1.3. StampedLock示例

Java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo01 {

    StampedLock stampedLock = new StampedLock();
    int x = 0, y = 0;
    int offsetX = 1, offsetY = 2;


    void move() {
        // 获取写锁
        long stamped = stampedLock.writeLock();
        x += offsetX;
        y += offsetY;
        System.out.println(Thread.currentThread().getName() + "移动到(" + x + "," + y + ")");
        // 释放写锁
        stampedLock.unlockWrite(stamped);
    }

    String readPoint() {
        // 获取一个版本号
        long stamped = stampedLock.tryOptimisticRead();
        int currentX = x;
        int currentY = y;
        // 验证在读取过程中,版本号是否改动,如果版本号被修改了,说明在读的过程中,有其他线程修改了x和y的值,
        // 此时会开启读锁,重新读取x 和 y的值。这里和CAS机制挺像的。
        if (!stampedLock.validate(stamped)) {
            stamped = stampedLock.readLock();
            currentX = x;
            currentY = y;
            stampedLock.unlockRead(stamped);
        }
        return "(" + currentX + "," + currentY + ")";
    }

    public static void main(String[] args) throws InterruptedException {
        StampedLockDemo01 stampedLockDemo01 = new StampedLockDemo01();

        // 分别开启三个写线程和三个读线程
        for (int i = 0; i < 3; i++) {
            Thread.ofVirtual().name("write_thread_" + i).start(() -> {
                stampedLockDemo01.move();
            });

            Thread.ofVirtual().name("read_thread_" + i).start(()->{
                String s = stampedLockDemo01.readPoint();
                System.out.println(Thread.currentThread().getName() + "读取到的位置是:" + s);
            });
        }

        TimeUnit.SECONDS.sleep(2);
    }
}

输出结果为:

scss 复制代码
write_thread_0移动到(1,2)
write_thread_1移动到(2,4)
write_thread_2移动到(3,6)
read_thread_1读取到的位置是:(2,4)
read_thread_2读取到的位置是:(2,4)
read_thread_0读取到的位置是:(2,4)

从上面的输出结果可以看出,写锁是互斥的,读锁是并发的。在 readPoint()方法中,我们stampedLock当前的版本号,然后开始读取数据,当读取完成后,我们校验stampedLock的版本号是否有修改,如果没有修改,则返回读取到的值,如果版本号有修改,则开启读锁重新获取数据。

下面是ReentrantLock、ReentrantReadWriteLock和StampedLock的比较:

特性 ReentrantLock ReentrantReadWriteLock StampedLock
是否支持可重入
是否支持乐观读
是否支持公平锁
编程复杂度 低,使用简单 低,使用简单 高,需手动验证乐观读有效性
适用场景 一般同步场景 读多写少非常合适 高并发读场景更合适,但使用需谨慎

2. 队列同步器

Java提供了一个队列同步器AbstractQueuedSynchronizer,它是基于模版方法模式设计的,这个类中提供了大量方法,让用户通过继承该类,可以实现自己的锁。

队列同步器的基本原理是:

  1. 通过定义一个同步状态state,加入state的初始值为0
  2. 所有的线程都通过CAS的方式来修改这个同步状态,如果线程A修改成功,将state设置为1,则认为线程A获取了锁
  3. 其他线程将尝试将state从0设置为1(通过CAS的方式),如果尝试失败,线程会被放到同步队列中
  4. 线程A退出同步块后,并将state设置为0,同时取同步队列的队首的线程,队首线程进入就绪状态
  5. 队首线程尝试通过CAS将state从0修改为1,如果修改成功,则队首线程获取锁,将队首线程从同步队列移除。否则将队首线程修改为阻塞状态。

我们先来分析一下AbstractQueuedSynchronizer类中的方法,这些方法包括了几个可以重写的方法和几个被final修饰的不可以重写的方法。

可以重写的方法:

方法名 描述
tryAcquire(int) 通过该方法尝试独占式获取同步状态,用户实现该方法需要查询当前状态并判断同步状态是否符合预期。
tryRelease(int) 通过该方法可以独占式的释放同步状态,等待获取同步状态的其他线程将有机会获取到同步状态
tryAcquireShared(int) 通过该方法尝试共享式获取同步状态,用户实现该方法需要查询当前状态并判断同步状态是否符合预期。
tryReleaseShared(int) 通过该方法可以共享式的释放同步状态,等待获取同步状态的其他线程将有机会获取到同步状态
isHeldExclusively() 判断同步状态是不是被当前线程独占

不可以重写的方法(这里只列举几个独占式获取同步状态的方法,共享式获取同步状态的方法就不一一列出了):

方法名 描述
acquire(int) 独占式的获取同步状态,如果当前线程独占式的获取同步状态成功,则由该方法返回,否则,当前线程会进入同步队列等待。该方法会调用上面重写的tryAcquire(int)方法
acquireInterruptibly(int) 该方法与acquire(int)一样,不过可以响应中断,当前线程如果没有获取同步状态进入同步队列,如果当前线程被中断,则该方法会抛出InterruptedException并返回
tryAcquireNanos(int,long) 在acquireInterruptibly(int)的基础上增加了一个超时设置,超过设定时间,还没有获取到同步状态的,将会返回false,否则返回true
release(int) 独占式的释放同步状态,该方法在释放同步状态之后,会将同步队列的队首线程唤醒

除了上述方法,AbstractQueuedSynchronizer类还提供了一些获取同步状态的方法:

  • getState():获取当前同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect, int update):通过CAS机制,设置当前的同步状态,可以保证原子性

2.1. ReentrantLock和队列同步器的关系

下面我们来看一下ReentrantLock的继承、组合关系:

从上图可以看出AbstractQueuedSynchronizer中有一个Node类,队列同步器中的节点就是通过该类实现的,该类有如下几个字段:

字段名 描述
Node prev 前驱节点,当前节点被加入同步队列时设置
Node next 指向后继结点的指针
Thread waiter 等待获取同步状态的线程
int status 节点状态,有如下状态:0:初始状态,什么也不表示-1:SIGNAL,表示当前节点的线程需要被唤醒,其后继节点会进入等待(阻塞)状态-2:CANCELLED,表示线程被取消,不会再被调度或唤醒(如超时、被中断)-3:CONDITION,节点在Condition 等待队列中(当线程在Condition上等待时,会进入等待队列,直到被其他线程用signal唤醒),而非同步队列1:PROPAGATE,用于共享模式下,下一次同步状态获取后继线程需要无条件传播(如读锁)

从上图可以看出,ReentrantLock包含了几个队列同步器(Sync、FairSync和NonfairSync),其锁能力主要是通过这几个队列同步器实现的。ReentrantLock可以实现公平锁和非公平锁,分别是通过FairSync和NonfairSync队列同步器实现的。

这里以ReentrantLock的lock()方法调用为例:

  1. ReentrantLock的lock()方法其实是通过调用FairSync(这里以FairSync为例)类提供的lock()方法实现的
  2. 在FairSync类的lock()方法中,会调用其自己的initialTryLock()方法,如果此时同步队列没有线程,则将当前线程设置为同步状态的独占者。
  3. 如果initialTryLock()方法返回false,则使用AbstractQueuedSynchronizer的不可修改方法acquire(int)去独占式的获取同步状态,如果获取到,则acquire(int)方法立刻返回,否则线程会被阻塞在acquire(int)方法

下面进行源码分析

initialTryLock()方法,该方法在FairSync 中实现,源码如下:

Java 复制代码
final boolean initialTryLock() {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取FairSync队列同步器的同步状态,同步状态默认为0
    int c = getState();
    // 如果同步状态为0,表示此时没有线程占有该同步状态
    if (c == 0) {
    //  hasQueuedThreads()判断同步队列中是否有线程阻塞在那里
    // compareAndSetState(0, 1) 通过CAS机制将同步状态从0设置为1
    // 如果条件成立,则将当前线程设置为同步状态的持有线程,意为这当前线程拿到锁
    if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(current);
        return true;
    }
    // 如果上面的条件不成立,判断持有同步状态的线程是不是当前线程
    // 如果是当前线程,则将同步状态加1,这意味着一个同步状态可以被同一个线程多次获取
    // 这也是实现可重入锁的关键,通过修改同步状态的值,我们可以让同一个线程重入多次
    } else if (getExclusiveOwnerThread() == current) {
    if (++c < 0) // overflow
        throw new Error("Maximum lock count exceeded");
    setState(c);
    return true;
    }
    return false;
}

acquire(int)方法的源码如下, 可以看到acquire(int)方法中,先调用了tryAcquire(arg)方法,再调用的重载的acquire(Node node, int arg, boolean shared,boolean interruptible, boolean timed, long time)方法:

Java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg))
        acquire(null, arg, false, false, false, 0L);
}

先看tryAcquire(arg)方法的源码,该方法我们知道在AbstractQueuedSynchronizer中是可以修改的方法,由FairSync提供了具体的实现:

Java 复制代码
protected final boolean tryAcquire(int acquires) {
    // hasQueuedPredecessors()判断当前线程是否有前驱线程,即同步队列是否有线程等待的时间更久
    // hasQueuedPredecessors()不需要我们自己实现,由AbstractQueuedSynchronizer提供实现
    // compareAndSetState(0, acquires) 设置同步状态
    // 如果条件都成立,当前线程获取同步状态,将当前线程设置为同步状态的持有者
    if (getState() == 0 && !hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

如果tryAcquire(arg)独占式获取同步状态失败,则调用AbstractQueuedSynchronizer提供的acquire(Node node, int arg, boolean shared,boolean interruptible, boolean timed, long time)方法,自旋的等待获取同步状态,acquire(Node node, int arg, boolean shared,boolean interruptible, boolean timed, long time)的大概逻辑如下:

  1. 判断node节点是不是同步队列的队首节点,如果不是,检查其前驱节点是否可以清除,如果可以清除,再次判断node是不是队首节点
  2. 如果node是队首节点,则尝试去获取同步状态,如果获取成功,将node节点设置为head节点,清空node节点绑定的线程
  3. 如果获取同步状态不成功,则将node添加到队尾,每隔一段时间执行一次循环
  4. 对于添加了超时时间限制的,会进行超时校验

同步队列中的每个线程都在自旋等待获取同步状态,直到获取同步状态或者被中断。

源码如下:

Java 复制代码
final int acquire(Node node, int arg, boolean shared,
                      boolean interruptible, boolean timed, long time) {
    Thread current = Thread.currentThread();
    byte spins = 0, postSpins = 0;   // retries upon unpark of first thread
    boolean interrupted = false, first = false;
    Node pred = null;               // predecessor of node when enqueued
    // 自旋等待获取同步状态
    for (;;) {
        // 判断当前节点的上一个节点是不是head,
        // 如果是head节点,则说明当前节点是第一个节点
        if (!first && (pred = (node == null) ? null : node.prev) != null &&
            !(first = (head == pred))) {
            // 如果node不是同步队列第一个节点,判断其前驱节点的状态是否小于0,
            // 如果是,清除前驱节点,然后继续循环,看看前驱清除清除后,当前node
            // 会不会成为队首节点
            if (pred.status < 0) {
                cleanQueue();           // predecessor cancelled
                continue;
            } else if (pred.prev == null) {
                Thread.onSpinWait();    // ensure serialization
                continue;
            }
        }
        // 如果node是同步队列的第一个节点
        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;
            }
            // 如果线程获取同步状态成功,并且node是同步队列的第一个节点,此时需要将node移除
            // 这里没有移除,而是让node成为新的head节点,将node指向的线程设置为null
            if (acquired) {
                if (first) {
                    node.prev = null; 
                    head = node;
                    // 这是其实pred == head 为true
                    pred.next = null;
                    node.waiter = null;
                    if (shared)
                        signalNextIfShared(node);
                    if (interrupted)
                        current.interrupt();
                }
                return 1;
            }
        }
        // 如果node不是同步队列的第一个节点
        Node t;
        // 如果tail都不存在,说明还没创建同步队列,先初始化一个同步队列
        if ((t = tail) == null) {           // initialize queue
            if (tryInitializeHead() == null)
                return acquireOnOOME(shared, arg);
        } else if (node == null) {          // allocate; retry before enqueue
            // 如果节点为空,则新建一个节点
            try {
                node = (shared) ? new SharedNode() : new ExclusiveNode();
            } catch (OutOfMemoryError oome) {
                return acquireOnOOME(shared, arg);
            }
        } else if (pred == null) {          // try to enqueue
            // 将新建的node节点添加为尾节点,并将当前线程放到这个节点中
            node.waiter = current;
            // 将node的前驱节点设置为尾节点
            node.setPrevRelaxed(t);         // avoid unnecessary fence
            if (!casTail(t, node))
                // 这里尝试将node设置为尾节点,如果失败,将node的前驱节点设置为NULL 
                node.setPrevRelaxed(null);  // back out
            else
                // 如果将node设置为尾节点成功,则让原尾节点的next指针指向新的尾节点
                t.next = node;
        } else if (first && spins != 0) {
            // 如果node已经变成了首节点,但是依然没竞争到同步状态,这时用Thread.onSpinWait()
            // 阻塞线程一会,避免一直自旋,浪费CPU性能
            --spins;                        // reduce unfairness on rewaits
            //  Thread.onSpinWait()可以优化自旋锁,让CPU可以处理其他线程
            Thread.onSpinWait();
        } else if (node.status == 0) {
            node.status = WAITING;          // enable signal and recheck
        } 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逻辑,然后跳出循环,也算是
            // 超时后直接返回,不再获取同步状态了 
            else
                break;
            node.clearStatus();
            if ((interrupted |= Thread.interrupted()) && interruptible)
                break;
        }
    }
    return cancelAcquire(node, interrupted, interruptible);
}

2.2. 队列同步器图解

上图展示了队列同步器的基本结构。线程如果没有获取到同步状态,就会进入同步队列,被追加到队尾。

  1. 首节点的变更是由拿到同步状态的线程进行的,不存在竞争,所以是线程安全的
  2. 队尾节点的追加,同一时间可能有多个线程被追加到队尾,是线程不安全的,需要使用CAS机制进行修改

3. 自定义锁

Java 复制代码
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;

public class SelfDefLock {

    // 自定义一个队列同步器,将AbstractQueuedSynchronizer类可以重写的方法重新实现
    private static class Sync extends AbstractQueuedSynchronizer {
        
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, arg)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }


        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            // 自旋检查
            for (; ; ) {
                // 获取当前同步状态
                int current = getState();
                int newState = current + arg;
                // 判断同步状态是否溢出或不符合业务逻辑
                if (newState < 0) {
                    return -1;
                }
                // 多线程的情况下,只有一个线程能通过这一步,其他线程得重新执行这个循环
                // 可以做到线程安全
                if (compareAndSetState(current, newState)) {
                    return newState;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            for (; ; ) {
                int current = getState();
                int newState = current - arg;
                if (newState < 0) {
                    return false;
                }
                if (compareAndSetState(current, newState)) {
                    return newState == 0;
                }
            }
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    // 这里实现的一个独占式锁,直接使用了AbstractQueuedSynchronizer的acquire()方法
    // 没有使用上面自己实现的tryAcquire方法
    public void lock() {
        sync.acquire(1);
    }
    
    // 尝试独占式的获取同步状态,
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

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

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

上面首先基于JDK提供的模版自定义了一个队列同步器,基于该自定义队列同步器创建了自定义锁。从上面的代码可以看出,自定义队列同步器中自己做的事情也很少,AbstractQueuedSynchronizer类封装的太好了。使用上面自定义的锁,创建一段示例代码:

Java 复制代码
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class SelfDefLockUse {

    public static void main(String[] args) throws InterruptedException {

        SelfDefLock selfDefLock = new SelfDefLock();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(()->{
            selfDefLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync code block");
            System.out.println(Thread.currentThread().getName() + " time is " + new Date());
            try {
                TimeUnit.SECONDS.sleep(6);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            selfDefLock.unlock();
        });

        TimeUnit.MICROSECONDS.sleep(10);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(()->{
            selfDefLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync code block");
            System.out.println(Thread.currentThread().getName() + " time is " + new Date());
            selfDefLock.unlock();
        });

        t1.join();
        t2.join();

    }
}

输出的结果为:

csharp 复制代码
t1_thread into sync code block
t1_thread time is Sun Jul 20 10:36:58 CST 2025
t2_thread into sync code block
t2_thread time is Sun Jul 20 10:37:04 CST 2025

可以看出,自定义的锁生效了,成功阻塞了t2_thread,直到t1_thread释放了同步状态。

4. 锁分类

锁从不同的维度,可以分成不同的锁。

  1. 按重入性分类:可重入锁和不可重入锁

我们知道JDK官方提供的ReentrantLock和ReentrantReadWriteLock都是可重入锁,可重入锁在这里的实现逻辑比较简单,只要允许同一个线程将同步状态不断地增加就可以了。不可重入的话,则不允许同一个线程对同步状态进行增加。

  1. 按公平性分类:公平锁和非公平锁

公平锁和非公平锁的区别是,当一个线程释放同步状态后,如果此时有新的线程过来竞争同步状态,这个新线程是否要判断此时同步队列是否有等待线程。如果是公平锁,在检测到同步队列有等待线程后,会将新线程追加到队列尾部。如果是非公平锁的话,新线程会直接过去竞争同步状态,竞争失败后,再追加到队列尾部。

ReentrantLock和ReentrantReadWriteLock都支持公平锁和非公平锁,可以看到这两个类里面分别都有FairSync和NonfairSync,可以分别实现公平锁和非公平锁功能。当我们初始化ReentrantLock和ReentrantReadWriteLock对象时,如果选择公平锁,则创建一个FairSync对象赋予里面的Sync引用,反之则创建一个NonfairSync对象赋予里面的Sync引用。

  1. 按共享性分类:独占锁和共享锁

ReentrantReadWriteLock类中定义了两个锁:ReadLock和WriteLock,其中ReadLock是共享锁,支持多个线程同时持有ReadLock(这也是其同步状态设置的,允许多个线程对其进行累加),但是一旦有线程持有ReadLock,则不再允许其他线程去持有WriteLock。WriteLock是互斥锁,同一时间,只允许一个线程获取WriteLock。一旦有线程持有WriteLock,便不再允许其他线程去获取ReadLock。

5. LockSupport工具

Java提供的一个阻塞和唤醒线程的工具LockSupport,LockSupport有一组公共静态方法,这些方法提供了纯粹阻塞和唤醒线程功能。他们和Thread的sleep()方法有点像,只不过sleep()方法不能被唤醒,只能被中断。下面是LockSupport的静态方法简介:

方法名称 功能描述
void park() 阻塞当前线程,如果有其他线程调用了unpark()方法,或者发起中断,才能从阻塞中退出。这里阻塞线程的时候,不会导致线程释放同步状态
void parkNanos(long nanos) 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
void parkUntil(long deadline) 阻塞当前线程,直到某个时间
void unpark(Thread thread) 唤醒处于阻塞状态的Thread
Java 复制代码
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo01 {


    public static void main(String[] args) throws InterruptedException {

        ReentrantLock reentrantLock = new ReentrantLock();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync block ");
            System.out.println(Thread.currentThread().getName() + " " + new Date());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " over sync block ");
            reentrantLock.unlock();
        });

        TimeUnit.MICROSECONDS.sleep(10);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            LockSupport.unpark(t1);
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync block ");
            System.out.println(Thread.currentThread().getName() + " " + new Date());
            reentrantLock.unlock();
        });

        t1.join();
        t2.join();
    }
}

输出结果为:

bash 复制代码
t1_thread into sync block 
t1_thread Sun Jul 20 18:07:59 CST 2025
t1_thread over sync block 
t2_thread into sync block 
t2_thread Sun Jul 20 18:08:04 CST 2025

6. Condition接口

6.1. Condition接口介绍

Condition接口提供的方法如上图所示,下面分别介绍每一个方法的作用:

方法 描述
void await() 在Condition上阻塞当前线程,直到其他线程发出signal信号或者中断。其底层其实也是基于LockSupport工具实现的
void awaitUninterruptibly() 在Condition上阻塞当前线程,不响应其他线程的中断信号
long await(long, TimeUnit) 在Condition上阻塞当前线程,和await()类似,增加了超时退出阻塞
boolean awaitUntil(Date) 在Condition上阻塞当前线程,直到某个时间
void signal() 用于唤醒在Condition上阻塞的某个线程
void signalAll() 用于唤醒在Condition上阻塞的所有线程

Condition接口的实现类ConditionObject位于AbstractQueuedSynchronizer类中,要通过类似ReentrantLock这样的锁来创建Condition对象,Condition对象和锁是绑定的。这个和java.lang.Object提供的监视器方法wait/notify/notifyAll方法挺类似的,是它们的一个替代品,不过两个在使用方式和特性上还是有区别的:

Object监视器方法和Condition接口的对比

对比项 Object监视器方法 Condition
前置条件 获取对象的锁 调用Lock.lock()获取锁调用Lock.newCondition()创建Condition对象
等待队列个数 一个 多个(因为可以通过Lock.newCondition()创建多个Condition对象)
当前线程阻塞后释放锁进入等待状态,不响应中断 不可以 可以
当前线程阻塞后释放锁,进入超时等待状态 可以 可以
当前线程阻塞后释放锁并等待到某个时间 不可以(是不是可以通过超时等待来实现) 可以
唤醒等待队列中的一个线程 可以 可以
唤醒等待队列中的全部线程 可以 可以

6.2. Condition接口示例

Java 复制代码
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo01 {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync code block");
            System.out.println(Thread.currentThread().getName() + " start time: " + new Date());
            try {
                condition.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                System.out.println(Thread.currentThread().getName() + " end time: " + new Date());
                reentrantLock.unlock();
            }
        });

        TimeUnit.MILLISECONDS.sleep(100);


        Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getName() + " into sync code block");
            System.out.println(Thread.currentThread().getName() + " start time: " + new Date());
            try {
                TimeUnit.SECONDS.sleep(5);
                condition.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                reentrantLock.unlock();
            }

        });

        t1.join();
        t2.join();
    }
}

上面代码,我们通过condition.await()阻塞了t1_thread是,在t2_thread中使用condition.signal()唤醒了t1_thread。

6.3. 通过Condition自定义阻塞队列

Java 复制代码
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SelfDefBlockedQueue {

    private Object[] items = new Object[5];
    private int addIndex = 0, removeIndex = 0, count = 0;

    private boolean addItem(Lock lock, Condition isFullCondition, Condition isEmptyCondition, Object item) {
        try {
            lock.lock();
            while (items.length == count) {
                isFullCondition.await();
            }
            ++count;
            items[addIndex] = item;
            addIndex = (addIndex + 1) % items.length;

            isEmptyCondition.signal();

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
            return true;
        }
    }

    private boolean removeItem(Lock lock, Condition isFullCondition, Condition isEmptyCondition) {

        try {
            lock.lock();
            while (count == 0) {
                isEmptyCondition.await();
            }
            --count;
            items[removeIndex] = null;
            removeIndex = (removeIndex + 1) % items.length;

            isFullCondition.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
            return true;
        }
    }


    public static void main(String[] args) throws InterruptedException {

        SelfDefBlockedQueue selfDefBlockedQueue = new SelfDefBlockedQueue();
        ReentrantLock lock = new ReentrantLock();
        Condition isFullCondition = lock.newCondition();
        Condition isEmptyCondition = lock.newCondition();


        for (int i = 0; i < 10; i++) {
            int finalI = i;
            Thread t = Thread.ofVirtual().name("add_thread_" + i).start(() -> {
                selfDefBlockedQueue.addItem(lock, isFullCondition, isEmptyCondition, String.valueOf(finalI));
            });

        }

        for (int i = 0; i <6; i++) {
            Thread t = Thread.ofVirtual().name("remove_thread_" + i).start(() -> {
                selfDefBlockedQueue.removeItem(lock, isFullCondition, isEmptyCondition);
            });
        }

        TimeUnit.SECONDS.sleep(2);
        Arrays.stream(selfDefBlockedQueue.items).forEach((o) -> {
            System.out.println(o);
        });
    }

}

输出结果为:

csharp 复制代码
null
7
6
8
9

上述代码中定义了一个addItem()方法和一个removeItem()方法,addItem()方法向队列中添加元素,removeItem()方法从队列中删除元素。

addItem()方法中,当数组已经满了的情况下,阻塞当前线程,直到另一个线程执行了removeItem()方法,将数组中的元素删除一个,然后将当前线程唤醒。

removeItem()方法中,当数组为空的情况下,阻塞当前线程,直到另一个线程执行了addItem()方法,向数组中添加一个元素,然后将当前线程唤醒。

6.4. Condition的等待队列

每个Condition对象都关联了一个等待队列,当一个线程调用了Condition的await()方法被阻塞后,线程会被封装成一个等待节点,添加到等待队列中(注意:同步队列和等待队列是不同的,线程一开始竞争同步状态失败是被放到同步队列中的,当线程获取到同步状态后,开始执行同步代码块,如果同步代码块中存在await()方法,则当前线程会被阻塞,进入Condition关联的等待队列中)。Condition等待队列的示意图如下所示:

等待队列中的线程的唤醒逻辑及唤醒后的处理逻辑如下:

  1. 首先等待队列的第一个节点将以线程安全的方式被添加到同步器的同步队列中,假设第一个节点关联线程A
  2. 调用LockSupport方法唤醒线程A,线程A从await方法的while循环退出
  3. 线程A开始调用同步器的acquire方法竞争同步状态
  4. 线程A获取同步状态成功后,从await方法退出,线程A继续执行其他逻辑
相关推荐
程序员是干活的25 分钟前
Java EE前端技术编程脚本语言JavaScript
java·大数据·前端·数据库·人工智能
某个默默无闻奋斗的人30 分钟前
【矩阵专题】Leetcode48.旋转图像(Hot100)
java·算法·leetcode
℡余晖^36 分钟前
每日面试题14:CMS与G1垃圾回收器的区别
java·jvm·算法
CDwenhuohuo1 小时前
滚动提示组件
java·前端·javascript
wei3872452321 小时前
集训总结2
java·数据库·mysql
Code季风1 小时前
Java 高级特性实战:反射与动态代理在 spring 中的核心应用
java·spring boot·spring
David爱编程1 小时前
final 修饰变量、方法、类的语义全解
java·后端
椒哥1 小时前
Open feign动态切流实现
java·后端·spring cloud
Code季风1 小时前
深入 Spring 性能调优:反射机制与动态代理的优化策略
java·spring·性能优化
RainbowSea1 小时前
购买服务器 + 项目部署上线详细步骤说明
java·服务器·后端