队列同步器的实现分析
自定义同步组件
目标:实现同一时刻只允许至多两个线程同时访问,超过两个线程的访问将堵塞工具命名为TwinsLock.
- 确定访问模式,同一时刻支持多个线程的访问,显然是共享式访问,需要使用同步器提供的acquireshared(int arge)方法和shared相关的方法。
- 定义资源数,TwinsLock在同一时刻允许至多两个线程同时访问,表明资源数必须为2。
- 可以初始化状态为2,当一个线程获取,status减1;该线程释放,status加1。状态的合法范围为 0(两个线程获取了资源)、1(一个线程获取资源)、2(没有线程获取资源)
- 组合自定义同步器。
- 自定义同步组件通过 自定义同步器来完成同步功能。 自定义同步器会被定义为自定义同步组件的内部类。
package com.qdb.thinkv.thread.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; public class TwinsLock implements Lock { private final Sync sync=new Sync(2); private static final class Sync extends AbstractQueuedSynchronizer{ Sync(int count) { if(count<=0){ throw new IllegalArgumentException("count must large than zero."); } setState(count); } public int tryAcquireShared(int reduceCount) { for(;;){ int current=getState(); int newCount=current-reduceCount; if(newCount<0 || compareAndSetState(current, newCount)){ return newCount; } } } public boolean tryReleaseShared(int returnCount){ for(;;){ int current=getState(); int newCount=current+returnCount; if(compareAndSetState(current, newCount)){ return true; } } } } public void lock() { sync.acquireShared(1); } public void unlock() { sync.releaseShared(1); } public void lockInterruptibly() throws InterruptedException { // TODO Auto-generated method stub } public boolean tryLock() { // TODO Auto-generated method stub return false; } public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { // TODO Auto-generated method stub return false; } public Condition newCondition() { // TODO Auto-generated method stub return null; } }
测试类
package com.qdb.thinkv.thread.lock; import java.util.ArrayList; public class TwinsLockTest { private TwinsLock lock=new TwinsLock(); private static ArrayList<Integer> arrayList=new ArrayList<Integer>(); public static void main(String[] args) throws InterruptedException { System.out.println("开始"); final TwinsLockTest test=new TwinsLockTest(); for(int i=0;i<5;i++){ new Thread(){ public void run(){ test.insert(Thread.currentThread()); }; }.start(); } } public void insert(Thread thread){ lock.lock(); try { System.out.println(thread.getName()+"得到了锁"); for(int i=0;i<5;i++){ arrayList.add(i); } } catch (Exception e) { System.out.println(e); }finally { System.out.println(thread.getName()+"释放了锁"); lock.unlock(); } } }
运行结果
总结:同步器作为一个桥梁,连接线程访问以及同步状态控制等底层技术与不同并发组件(比如lock、CountDownLatch等)的接口语义。
独占式超时获取同步状态
介绍
通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态。
即在指定的时间段内获取同步状态,如果获取到同步状态则返回true.
否则,返回false。(传统Java同步操作不具备的特性如synchronized)
历史:
在java5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待获取锁。
在java5中,同步器提供了acquireInterruptibily(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterruptedException.
doAcquireNanos(int arg,long nanosTimeout)超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”。
分析
doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。
针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTImeout,为了防止过早通知,
nanosTimeout计算公式为:nanosTimeout -= now - lastTime,
nanosTImeout为需要睡眠的时间间隔
now为当前唤醒时间
lastTime为上次唤醒时间
如果nanosTimeout大于0时则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时。
/** * Acquires in exclusive timed mode. * * @param arg the acquire argument * @param nanosTimeout max wait time * @return {@code true} if acquired */ private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { long lastTime = System.nanoTime(); final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC 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); } }
/** * 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; }
考虑 独占式超时获取同步状态与独占式获取同步状态的区别是什么?
共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于统一时刻能否有多个线程同时获取到同步状态。
依文件的读写为例,
如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被堵塞,而读操作能够同时进行。
写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况。
左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被堵塞,
右半部分是独占式访问资源时 ,同一时刻其他访问均被堵塞。
/**
* Acquires in shared mode, ignoring interrupts. Implemented by
* first invoking at least once {@link #tryAcquireShared},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquireShared} until success.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { 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); } }
在acquireShard(int arg) 方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg) 方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取同步状态并退出自旋的条件就是tryAcquireShard(int arg)方法返回值大于等于0.可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为 头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于现场获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步状态队列移出。
首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占时Node.EXCLUSIVE,同一时刻只能有一个线程获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点依思循环的方式获取同步状态。如果获取不到则堵塞节点中的线程,而被阻塞线程的唤醒主要依据前驱节点的出队或堵塞线程被中断来实现。
看下代码细节
/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire是继承类中自定义的逻辑
/** * 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; }
/** * 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; } } } }
在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设。可以看出,end(final Node node)方法将并发添加节点的请求通过CAS 变得“串行化”了
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ 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; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
在acquireQueued(final Node node,int arg) 方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么,原因有两个
第一:头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
第二:维护同步队列的FIFO原则。
由于非首节点的线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。
可以看到节点和节点之间的在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则返回FIFO,并且也便于
对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
总结:在获取同步状态时,同步器维护同一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
移出队列的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后续节点。
同步队列