
本文共 4812 字,大约阅读时间需要 16 分钟。
在上一篇《Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析》中,我们介绍了AbstractQueuedSynchronizer(AQS)基本的概念,重点阐述了AQS的排队区实现方式,包括独占模式和共享模式的概念以及结点的等待状态。掌握这些内容是后续分析AQS源码的基础,因此建议读者在阅读本文之前回顾上一篇文章。
本文将重点探讨在独占模式下,结点如何进入同步队列排队,以及离开同步队列之前的操作流程。
AQS为在独占模式和共享模式下提供三种获取锁的方式:不响应线程中断的获取、响应线程中断的获取以及设置超时时间的获取。这三种方式在实现上大致相同,区别仅在于细节处理部分。本文将以不响应线程中断的方式为例,重点分析其他两种方式的不同之处。
1、不响应线程中断的锁获取方式
不响应线程中断的获取方式主要通过acquire
方法实现,代码如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); }}
该方法由四个步骤组成:首先尝试获取锁,未能成功后进入排队区,最后可能自中断。
第一步:尝试获取锁
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException();}
这个方法是子类实现的,用于判断锁是否可用。这一步相当于敲门,如果门没锁(tryAcquire返回true),就直接进入,否则进入下一步。
第二步:进入排队区
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(Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // 协助GC failed = false; return interrupted; // 唤醒后续操作 } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { if (failed) { cancelAcquire(node); } }}
进入排队区后,这个方法会尝试在同步队列中寻找可获取锁的位置。如果前继结点是头结点且尝试获取锁成功,则头结点更新为当前结点,前继结点指针清空。若获取锁失败,判断是否需要挂起当前线程,决定是否使用parkAndCheckInterrupt
方法进行挂起。
第四步:自中断
private static void selfInterrupt() { Thread.currentThread().interrupt();}
如果在获取锁过程中没有成功获取锁,线程会被中断挂起。
2、响应线程中断的锁获取方式
响应线程中断的方式通过doAcquireInterruptibly
方法实现,代码如下:
private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}
与不响应线程中断的方式相比,响应线程中断方式在获取锁失败时会抛出InterruptedException
异常,这样线程可以及时被中断并处理。
3、设置超时时间的锁获取方式
设置超时时间的方式通过doAcquireNanos
方法实现,代码如下:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { long lastTime = System.nanoTime(); final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; failed = false; return true; } if (nanosTimeout <= 0) { return false; } if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { LockSupport.parkNanos(this, nanosTimeout); } long now = System.nanoTime(); nanosTimeout -= now - lastTime; lastTime = now; if (Thread.interrupted()) { throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } }}
超时设置锁获取方式会在超时时间到达时退出循环或挂起线程。每次循环会减少超时时间,并检查线程是否中断。
4、线程释放锁并离开同步队列的方式
释放锁的过程主要通过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) { if (node.waitStatus < 0) { compareAndSetWaitStatus(node, node.waitStatus, 0); } Node s = node.next; if (s == null || s.waitStatus > 0) { for (Node t = tail; t != node; t = t.prev) { if (t.waitStatus <= 0) { s = t; break; } } if (s != null) { LockSupport.unpark(s.thread); } }}
当线程持有锁时,它会尝试释放锁。如果头结点不为空且前一直状态不为0,则唤醒后继结点。唤醒后继结点时,会通知线程后续是否有需要继续挂牌的情况。
总结
AQS为Java并发编程提供了强大的工具之一,其独占模式下的获取和释放锁机制通过同步队列实现,允许线程在不响应中断或响应中断的条件下进行获取。理解这些机制对深入掌握并发编程至关重要。
发表评论
最新留言
关于作者
