基于AQS实现一个自定义的锁
java并发编程中,锁自然其中的必须的产物。而在java的容器框架中,也提供了满足各种场景的锁。但是,有一个共性就是,他们都是基于AbstractQueuedSynchronizer(AQS)。可见AQS的重要性!
下面,让我们也来基于AQS实现一个自己的锁!
public class TwinsLockTest { @Test public void testTwinsLock() { final Lock lock = new TwinsLock(); class Worker extends Thread { @Override public void run() { while (true) { // 获取锁 lock.lock(); try { SleepUtils.second(1); System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName()); SleepUtils.second(1); } finally { // 释放锁 lock.unlock(); } } } } // 开10个线程运行worker, 如果没有锁,应该是几乎同时很快完成 // 但 TwinsLock 只允许同时有两个线程获得锁运行 for (int i = 0; i < 10; i++) { Worker w = new Worker(); w.setDaemon(true); w.start(); } // 每隔1s换行 for (int i = 0; i < 10; i++) { SleepUtils.second(1); System.out.println(); } } } /** * 双资源锁 */ class TwinsLock implements Lock { private final Sync sync = new Sync(2); private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -8540764104913403569L; Sync(int count) { if (count <= 0) { throw new IllegalArgumentException("锁资源数不能为负数~"); } // 调用 AQS 设置资源总数,备用 setState(count); } @Override public int tryAcquireShared(int reduceCount) { // cas 获取锁 // 由 AQS 的 acquireShared -> doAcquireShared 调用 for (; ; ) { int current = getState(); int newCount = current - reduceCount; if (newCount < 0 || compareAndSetState(current, newCount)) { return newCount; } } } @Override public boolean tryReleaseShared(int returnCount) { // cas 释放锁 // 由AQS releaseShared -> doReleaseShared 调用 for (; ; ) { int current = getState(); int newState = current + returnCount; if (compareAndSetState(current, newState)) { return true; } } } } @Override public void lock() { sync.acquireShared(1); } @Override public void unlock() { sync.releaseShared(1); } // 忽略,如要实现,直接调用 AQS @Override public boolean tryLock() { return false; } // 忽略,如要实现,直接调用 AQS @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } // 忽略,如要实现,直接调用 AQS @Override public void lockInterruptibly() throws InterruptedException { } // 忽略,如要实现,直接调用 AQS @Override public Condition newCondition() { return null; } } // 睡眠工具类 class SleepUtils { public static void second(int sec) { try { Thread.sleep(sec * 1000L); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出的结果是,每两个线程同时执行,10个中挑两个线程,也就是10个任务花5秒钟完成,从而达到资源数量限制的目的。
下面我们来分析下 lock 的运行原理!
首先,调用 lock.lock(), 获得锁,该lock返回值为void, 所以怎么获取锁呢?自然是在没有获取到锁的时候,自己进行阻塞了!
调用lock()方法后,lock调用了AQS中的 acquireShared(), 可见,具体实现方法是在 acquireShared() 中,如下:
public final void acquireShared(int arg) { // 先尝试获取 shared 锁,tryAcquireShared() 由具体的实现类处理,如果返回小于0则进入竞争状态 // 如果大于0,说明资源还有多余的,直接进入后续操作 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
而咱们自定义实现的 TwinsLock 实现获取锁方式为cas获取,从而达到阻塞的效果:
@Override public int tryAcquireShared(int reduceCount) { // cas 获取锁 // 由 AQS 的 acquireShared 调用 for (; ; ) { int current = getState(); int newCount = current - reduceCount; if (newCount < 0 || compareAndSetState(current, newCount)) { return newCount; } } }
但是,对于剩余资源数小于0的情况,直接返回,那么是不是就不能阻塞锁了呢?答案是,在AQS中,会有另一个阻塞操作, doAcquireShared()
/** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { // 先将线程加入等待队列中,类型为 SHARED final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { // 获取上一个等待中的线程 final Node p = node.predecessor(); // 如果是头节点,那么就可以尝试获取锁,也就是说,每次只会取头节点线程进行调用,即先到先得FIFO规则,公平锁 if (p == head) { // 调用子类具体实现,获取共享锁 int r = tryAcquireShared(arg); if (r >= 0) { // 获取到锁后,将head设置过 setHeadAndPropagate(node, r); p.next = null; // help GC // 如果上次捕获到中断信息,则进行中断响应 if (interrupted) selfInterrupt(); failed = false; return; } } // 获取锁失败后,检测中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 如果获取锁失败,则 if (failed) cancelAcquire(node); } }
我们从下面的代码中看到具体是怎么添加队列,怎么进行中断检测的:
/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } /** * CAS tail field. Used only by enq. */ private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } /** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } /** * Checks and updates status for a node that failed to acquire. * Returns true if thread should block. This is the main signal * control in all acquire loops. Requires that pred == node.prev. * * @param pred node's predecessor holding status * @param node the node * @return {@code true} if thread should block */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } /** * Sets head of queue, and checks if successor may be waiting * in shared mode, if so propagating if either propagate > 0 or * PROPAGATE status was set. * * @param node the node * @param propagate the return value from a tryAcquireShared */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } } /** * Release action for shared mode -- signals successor and ensures * propagation. (Note: For exclusive mode, release just amounts * to calling unparkSuccessor of head if it needs signal.) */ private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } /** * Cancels an ongoing attempt to acquire. * * @param node the node */ private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; // help GC } }
以上是获取锁的过程,锁得到后,就可以后续处理。最后,释放锁: unlock(), 调用 AQS 的releaseShared 。
/** * Releases in shared mode. Implemented by unblocking one or more * threads if {@link #tryReleaseShared} returns true. * * @param arg the release argument. This value is conveyed to * {@link #tryReleaseShared} but is otherwise uninterpreted * and can represent anything you like. * @return the value returned from {@link #tryReleaseShared} */ public final boolean releaseShared(int arg) { // 调用子类实现,如果成功,再进入AQS逻辑,否则释放失败 if (tryReleaseShared(arg)) { // AQS 释放 doReleaseShared(); return true; } return false; }
可以看到,AQS已经提供了很方便的基础锁设施,我们要实现自定义的锁,只需重写几个特定的方法即可。
jdk中,基于AQS实现的锁有: ReentrantLock 可重入锁, ReadWriteLock 读写锁, Semaphore 信号量, CountDownLatch 闭锁; 各尽其用吧!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
2015-12-02 报警系统:php输出头信息以方便脚本抓取信息[排查篇]