JAVA之AQS

本文介绍了一位开发者如何实现一个多键版本的ReentrantLock,支持基于key的加锁和释放。通过继承AbstractQueuedSynchronizer,实现了tryAcquire和tryRelease方法,并针对key的管理进行了特殊设计,确保线程安全。文章详细阐述了实现过程中的关键步骤、问题及解决方案,提供了一个演示示例代码,展示了如何在多线程环境中正确地同步和解锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

由于去年12月底刚换工作,再加上今年3月份上海开始封控;一直处于高度紧张的状态下,没法静下心来好好geek。

AQS这个主题也是老生常谈了,面试中也经常遇到;但大多资料都是理论上的,各个属性是什么意思以及源码分析,工作中也是使用AbstractQueuedSynchronizer的实现类,所以想通过自己实现一个AQS来加深对其的把握。

这里想实现一个多key的ReentrantLock,支持以key进行加/释放锁。效果可以参考redissetnx, 区别是可以自动唤醒,setnx可能还需要配合自旋。

PS: 下面实现的只是一个demo,只是为了加深对AQS的理解。

实现

STEP1

要制作自己的AQS首先要继承AbstractQueuedSynchronizer并且实现以下几个方法:

public class MultiReentrantLock extends AbstractQueuedSynchronizer {
    ...
    // 是否可以拿到锁,false的话会进入队列,当前Thread也会挂起
    protected boolean tryAcquire(int arg) {
        return super.tryAcquire(arg);
    }

    // 是否可以释放,true的话激活第一个等待的节点
    protected boolean tryRelease(int arg) {
        return super.tryRelease(arg);
    }

    // 共享锁
    protected int tryAcquireShared(int arg) {
        return super.tryAcquireShared(arg);
    }

    // 释放共享锁
    protected boolean tryReleaseShared(int arg) {
        return super.tryReleaseShared(arg);
    }
}

STEP2

注意到tryAcquiretryRelease只有一个int类型的入参,并不支持我们传递一个key进来进行判断。ThreadLocal是一种实现方式,在tryAcquire之前设置,在tryRelease中获取,并进行一些逻辑判断,最后在release之后删除;但是在编写中发现,ThreadLocal不能满足我的需求(与STEP3提到的问题有关),因为在tryRelease阶段,需要在A线程的上下文去获取跟B线程绑定的key。所以在实际的demo里,是将key设置到thread name中的。

public class MultiReentrantLock extends AbstractQueuedSynchronizer {
    ...
    public void lock(String lockKey) {
        Thread.currentThread().setName(lockKey);
        acquire(1);
    }
    ...
}

STEP3

这里会有一个问题,我们重温下acquirerelease的逻辑:

acquire

    public final void acquire(int arg) {
        // 1. 判断是否可以拿到锁
        // 2. 如果不可以的话,将当前节点加入队列进行等待
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    // 创建节点并将其添加到队列尾部
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // 如果前节点是head节点,并且当前可以拿到锁就将自己设置为头节点
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果还需要等待,先将节点状态设置为`SIGNAL`(等待信息)
                // 然后再将当前现场挂起
                // 当线程重新可运行时,判断其状态是否为interrupted,是的话会执行`selfInterrupt`
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果有异常失败了,则将该节点从队列中移除
            if (failed)
                cancelAcquire(node);
        }
    }

release

    public final boolean release(int arg) {
        // 如果可以释放,则激活下一个节点
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
    // 这里LockSupport.unpark(s.thread)后就会转到acquireQueued并执行以下逻辑
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                ...
            }
        } finally {
            ...
        }
    }

问题

STEP3开头提到有一个问题,在这里说明下:

acquire需要加入队列时,添加到队列的节点是无key信息的;release也一样,只是激活下一个节点的线程,并不是激活与当前节点有相同key的节点。所以需要在tryAcquiretryRelease中有一些逻辑判断:

  1. tryAcquire中,我们需要判断是否有相同的keythread在执行中或者在等待中?如果没有,就直接执行;否则需要加入等待队列;
  2. tryRelease中,我们需要判断下一个待激活的节点绑定的key释放跟当前要释放的一样?如果一样,就返回true激活下一个节点;否则,就需要再判断下一个待激活的节点所关联的key是否有其他线程正在执行,如果没有就激活;否则不激活。

所以在这里,我们再将key所关联的所有Thread保存在一个数组里。

public class MultiReentrantLock extends AbstractQueuedSynchronizer {

    ConcurrentHashMap<String, LinkedList<Thread>> lockQueueHolders = new ConcurrentHashMap<>();
}

完整代码展示

public class MultiReentrantLock extends AbstractQueuedSynchronizer {

    ConcurrentHashMap<String, LinkedList<Thread>> lockQueueHolders = new ConcurrentHashMap<>();

    public MultiReentrantLock() {
    }

    public void lock(String lockKey) {
        Thread.currentThread().setName(lockKey);
        acquire(1);
    }

    public void unlock(String lockKey) {
        if (null == lockKey) {
            return;
        }
        // 要释放的key与自身关联的不一致,抛异常
        if (!lockKey.equals(Thread.currentThread().getName())) {
            throw new UnsupportedOperationException();
        }
        release(1);
    }

    @Override
    protected boolean tryAcquire(int arg) {
        LinkedList<Thread> threadQueue = lockQueueHolders.getOrDefault(Thread.currentThread().getName(), new LinkedList<>());
        boolean ret = false;
        // 如果该key还未设置threadQueue或者第一个thread与当前thread相同,暂时判定结果为true
        if (threadQueue.isEmpty()) {
            ret = true;
            addToLockQueue(threadQueue);
        } else if (threadQueue.getFirst() == Thread.currentThread()) {
            ret = true;
        } else {
            addToLockQueue(threadQueue);
        }
        LinkedList<Thread> curThreadQueue = lockQueueHolders.putIfAbsent(Thread.currentThread().getName(), threadQueue);
        // 如果有别的线程提早将相同key对应的threadQueue设置到Map中,则判定为失败
        // 同时将其加入到已有的threadQueue中
        if (null != curThreadQueue && curThreadQueue != threadQueue) {
            addToLockQueue(curThreadQueue);
            ret = false;
        }
        return ret;
    }

    private void addToLockQueue(LinkedList<Thread> threadQueue) {
        // 由于tryAcquire会多次执行,所以需要判断下当前的thread是否已经在等待队列中
        if (!isQueued(Thread.currentThread())) {
            threadQueue.add(Thread.currentThread());
        }
    }

    @Override
    protected boolean tryRelease(int arg) {
        String localKey = Thread.currentThread().getName();
        // 如果thread队列第一个元素是当前thread,暂定可以激活下一个节点,并将其从该key的threadQueue中移出
        if (lockQueueHolders.get(localKey).getFirst() == Thread.currentThread()) {
            lockQueueHolders.get(localKey).removeFirst();
        } else {
            return false;
        }
        // 获取第一个待激活节点关联的Thread
        Thread firstQueuedThread = getFirstQueuedThread();
        // 如果没有下一个待激活节点,直接返回true
        if (null == firstQueuedThread) {
            return true;
        }
        // 通过getName获取该Thread关联的key
        // 上文中提到ThreadLocal不满足要求,就是在这个地方
        String firstQueuedKey = firstQueuedThread.getName();
        // 如果第一个等待的节点与当前释放锁的节点的key相同,则可以释放
        if (localKey.equals(firstQueuedKey)) {
            return true;
        }
        // 如果下一个待激活的节点关联的Thread,是该key的threadQueue的第一个,表示可以激活
        return firstQueuedThread == lockQueueHolders.get(firstQueuedKey).getFirst();
    }

    @Override
    protected int tryAcquireShared(int arg) {
        return 1;
    }

    @Override
    protected boolean tryReleaseShared(int arg) {
        return true;
    }
}

执行

public class MainRunner {
    public static void main(String[] args) {
        TestCase testCase = new TestCase();
        CaseRunner runner1 = new CaseRunner(testCase, "A", 1, 1000);
        CaseRunner runner2 = new CaseRunner(testCase, "B", 1, 2000);
        CaseRunner runner3 = new CaseRunner(testCase, "C", 1, 3000);
        CaseRunner runner4 = new CaseRunner(testCase, "A", 2, 1000);
        CaseRunner runner5 = new CaseRunner(testCase, "A", 3, 1000);
        CaseRunner runner6 = new CaseRunner(testCase, "C", 2, 3000);
        CaseRunner runner7 = new CaseRunner(testCase, "B", 2, 2000);

        runner1.start();
        runner2.start();
        runner3.start();
        runner4.start();
        runner5.start();
        runner6.start();
        runner7.start();
    }

    static class TestCase {
        MultiReentrantLock multiReentrantLock = new MultiReentrantLock();

        public void tCase(String key, int counter, long sleepTime) throws InterruptedException {
            System.out.println(System.currentTimeMillis() + " key: " + key + '-' + counter + "开始获取锁");
            multiReentrantLock.lock(key);
            System.out.println(System.currentTimeMillis() + " key: " + key + '-' + counter + "获取锁");
            Thread.sleep(sleepTime);
            System.out.println(System.currentTimeMillis() + " key: " + key + '-' + counter + "开始释放锁");
            multiReentrantLock.unlock(key);
            System.out.println(System.currentTimeMillis() + " key: " + key + '-' + counter + "释放锁");
        }
    }

    static class CaseRunner extends Thread {
        private TestCase testCase;
        private String lockKey;
        private long sleepTime;
        private int counter;

        public CaseRunner(TestCase testCase, String lockKey, int counter, long sleepTime) {
            this.testCase = testCase;
            this.lockKey = lockKey;
            this.sleepTime = sleepTime;
            this.counter = counter;
        }

        @Override
        public void run() {
            try {
                testCase.tCase(lockKey, counter, sleepTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

结果打印

1656154306290 key: A-1开始获取锁
1656154306291 key: B-1开始获取锁
1656154306291 key: C-1开始获取锁
1656154306291 key: B-1获取锁
1656154306291 key: A-1获取锁
1656154306291 key: C-1获取锁
1656154306293 key: A-2开始获取锁
1656154306293 key: A-3开始获取锁
1656154306294 key: C-2开始获取锁
1656154306294 key: B-2开始获取锁
1656154307292 key: A-1开始释放锁
1656154307292 key: A-1释放锁
1656154307292 key: A-2获取锁
1656154308292 key: B-1开始释放锁
1656154308292 key: B-1释放锁
1656154308292 key: A-2开始释放锁
1656154308292 key: A-2释放锁
1656154308292 key: A-3获取锁
1656154309293 key: A-3开始释放锁
1656154309293 key: C-1开始释放锁
1656154309293 key: A-3释放锁
1656154309293 key: C-1释放锁
1656154309293 key: C-2获取锁
1656154312299 key: C-2开始释放锁
1656154312299 key: C-2释放锁
1656154312299 key: B-2获取锁
1656154314300 key: B-2开始释放锁
1656154314300 key: B-2释放锁
<think>好的,我现在需要为用户解析Java中的AQS(AbstractQueuedSynchronizer)。首先,我得回忆一下AQS的基本概念和作用。AQSJava并发包中的一个核心框架,很多同步器比如ReentrantLock、Semaphore都是基于它实现的。用户可能想了解它的工作原理、结构以及如何使用。 接下来,我应该从AQS的设计目的说起,解释它如何通过一个FIFO队列来管理线程的阻塞和唤醒。然后,需要提到它使用了一个volatile的int变量来表示同步状态,这是实现各种同步机制的基础。 然后,结构部分要分点说明,包括同步状态、CLH队列、节点类型(独占和共享)。每个部分要简明扼要,比如同步状态是通过getState()、setState()和compareAndSetState()方法来操作的。 接下来,关键方法部分,用户可能想知道如何自定义同步器,所以需要提到tryAcquire、tryRelease等方法,并举例说明独占模式和共享模式的区别。比如CountDownLatch和Semaphore的不同实现方式。 然后,内部实现需要详细一点,比如CLH队列的变种,节点如何队和出队,以及自旋和阻塞的机制。这部分可能需要解释acquireQueued方法中的循环和park操作。 应用场景部分,列举常见的基于AQS的类,帮助用户理解其实际用途。比如ReentrantLock、ReentrantReadWriteLock等,说明它们各自的特点和如何利用AQS实现的。 最后,自定义同步器的步骤,给出一个简单的例子,比如实现一个非重的独占锁,展示如何重写tryAcquire和tryRelease方法。这样用户能更直观地理解如何应用AQS。 需要注意的是,用户可能对某些技术细节不太熟悉,比如CLH队列或者CAS操作,所以在解释时要避免过于深,保持通俗易懂。同时,确保代码示例正确,符合Java的语法和AQS的使用规范。 另外,要检查是否所有数学公式和代码都按照要求格式化,行内公式用$...$,独立公式用$$...$$,虽然这个问题可能不涉及太多数学内容,但如果有状态表示之类的可能需要用LaTeX。不过在这个问题中,可能主要是代码和术语,所以重点放在结构清晰和正确性上。 最后,总结AQS的重要性和优势,强调其作为Java并发基础组件的作用,帮助用户形成整体的理解。</think>Java中的AQS(AbstractQueuedSynchronizer)是并发编程的核心框架,用于构建锁和同步器。以下是逐步解析: --- ### **1. AQS的作用** AQS通过**同步状态管理**和**线程排队机制**,为开发者提供了实现**独占锁**(如ReentrantLock)和**共享锁**(如Semaphore)的底层支持。 --- ### **2. 核心结构** AQS依赖两个关键组件: - **同步状态(State)** 用`volatile int`表示,通过以下方法操作: ```java getState() // 获取当前状态 setState(int) // 直接设置状态 compareAndSetState(int expect, int update) // CAS原子操作 ``` - **CLH队列** 一个FIFO双向队列,存储等待线程的节点(Node)。 --- ### **3. 节点类型(Node)** 每个线程被封装为队列节点,包含两种模式: - **独占模式(EXCLUSIVE)** 同一时刻仅允许一个线程获取资源(如ReentrantLock)。 - **共享模式(SHARED)** 允许多个线程同时获取资源(如Semaphore)。 --- ### **4. 关键方法** 开发者需重写以下方法实现自定义同步器: - **独占模式** ```java protected boolean tryAcquire(int arg) // 尝试获取资源 protected boolean tryRelease(int arg) // 尝试释放资源 ``` - **共享模式** ```java protected int tryAcquireShared(int arg) // 返回值≥0表示成功 protected boolean tryReleaseShared(int arg) ``` --- ### **5. 内部工作流程** #### **获取资源(以独占模式为例)** 1. 调用`tryAcquire`尝试直接获取资源。 2. 若失败,将线程封装为Node加队列尾部。 3. 进自旋循环,检查前驱节点是否为头节点: - 是则再次尝试`tryAcquire`。 - 否则调用`LockSupport.park()`阻塞线程。 #### **释放资源** 1. 调用`tryRelease`释放资源。 2. 若成功,唤醒队列中下一个有效节点。 --- ### **6. 应用场景** AQS是以下同步器的基础: - **ReentrantLock** 通过重写`tryAcquire`实现可重锁。 - **Semaphore** 使用共享模式控制资源访问数量。 - **CountDownLatch** 利用共享模式实现线程等待。 - **ReentrantReadWriteLock** 分离读(共享)和写(独占)锁。 --- ### **7. 自定义同步器示例** 实现一个非重的独占锁: ```java class NonReentrantLock extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { // CAS设置状态 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); // 无需CAS,只有持有线程能调用release return true; } } ``` --- ### **8. 优势与总结** - **灵活性**:通过重写少数方法即可实现复杂同步逻辑。 - **高性能**:CLH队列减少锁竞争,CAS操作保证原子性。 - **标准化**:JDK内置同步器均基于AQS,代码复用性高。 AQSJava并发包的基石,理解其原理能帮助开发者更高效地处理多线程问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值