时间记录:2019-4-22
一:什么是抽象队列同步器
提供用于实现阻塞锁和相关依赖先进先出(FIFO)等待队列的同步器(信号量、事件等)。此类被设计为大多数类型的同步器的有用基础,这些同步器依赖单个原子int值来表示状态。子类必须定义更改此状态的受保护方法,以及定义此状态对于正在获取或释放的对象意味着什么。考虑到这些,这个类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态字段,但只有使用方法getstate、setstate和compareandsetstate对原子更 int值进行同步跟踪。从字面上来看队列同步器,首先有一个队列,然后用队列做线程的同步作用。
AQS同步队列主要的就是利用一种存在的资源,而且这种资源是单一使用的,可以回收可以分配,然后将线程或者说操作和资源绑定达到了同步的目的。
二:抽象同步队列如何实现
开始提到了一种资源,那么在AQS中这个资源就是一个队列,正好和名字完美的匹配,抽象队列同步器。那么是如果进行同步的呢,说道了资源的分配。举个例子吧,比如十个人去澡堂洗澡,然后澡堂只有三个位置可以洗,先一个人去洗,然后占有一个位置,接下来的两个再次占有两个位置然后,第四个人没有位置了,就会变成等待的状态,只有等到有人洗完出来后第四个人才能继续洗,这种就是抽象同步器的大体的实现思路,具体的还是要自己考虑的。
注意这里说的队列就是值三个位置,也就是资源,当一个人结束了 这个资源就回收了,然后再分配到其他的人,那如果开始的三个人一直不结束会造成什么样的结果呢?那么后面的就会一直等待。这样的处理方式是不是就可以进行同步了。假设A,B,C三个事务,C只有在A,B均完成的情况下才能执行,那么这个时候就可以采取这样的方式进行了。这里要注意的是结束和开始的问题,也就是请求资源和释放资源的调节。具体的实现还是需要根据具体的要求实现,jdk中目前已经实现的有Countdownlatch,Semaphore,CyclicBarrier 等,他们具体的实现也是不一样的。
思考如果存在这样的一个队列,那么是不是可以进一些特殊的操作,事件等待,处理限流,线程等待等。
我们来看下AQS的源码是如何实现的吧!
首先我们提到了队列,那么在AQS中是有队列吗?但是我们查看源码并没有看到eqeue之类的东西,但是存在一个Node 这里的node是一个数据结构,通过node链接起来形成一个类似于队列的东西(个人是这样理解的)
Node
-----------------------------------------------
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus. Assigned during enqueuing, and nulled
* out (for sake of GC) only upon dequeuing. Also, upon
* cancellation of a predecessor, we short-circuit while
* finding a non-cancelled one, which will always exist
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release. Assigned during enqueuing, adjusted
* when bypassing cancelled predecessors, and nulled out (for
* sake of GC) when dequeued. The enq operation does not
* assign next field of a predecessor until after attachment,
* so seeing a null next field does not necessarily mean that
* node is at end of queue. However, if a next field appears
* to be null, we can scan prev's from the tail to
* double-check. The next field of cancelled nodes is set to
* point to the node itself instead of null, to make life
* easier for isOnSyncQueue.
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
-----------------------------------------------
以上就是对这个node数据结构的定义,我们来看下结构的具体含义
SHARED:标记于**共享模式**下的等待
EXCLUSIVE:标记用于 **独占模式**下的等待
waitStatus:表示该节点的状态
thread:表示与该节点绑定的线程
prev:表示该节点的前置节点,即排在当前的队列前面的节点,也是前面需要执行的线程
nextWaiter:表示在此节点之后的节点,也就是排在这个队列后面的节点,即后面需要执行的线程
节点的状态:
SIGNAL:表示当前的节点在释放或者是取消的时候,将当前节点后面的节点都必须取消
CANCELLED:表示由于超时(给定的等待时间)或中断,此节点取消,对应的节点的线程不在阻塞
CONDITION:表示当前位于条件队列中。在传输之前,它不会用作同步队列节点,此时状态将设置为0。
PROPAGATE:releaseShared应该传播到其他节点。这是在DoreleaseShared中设置的(仅用于头节点),以确保传播继续,即使其他操作已经
干预。
0:以上的情况均不是
-----------------------------------------------
通过以上的Node节点接口来看已经很明显了,不同的节点和不同的线程进行绑定,然后线程的顺序是用过节点的顺序进行的,
线程是否需要同步则通过节点的状态来决定,那么AQS其余的操作主要是对这些的节点的操作,我们继续往下看。
AQS的成员
-----------------------------------------------
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
-----------------------------------------------
head:等待队列的头,已延迟初始化。除了初始化,它只能通过方法sethead进行修改 注意:如果head存在,则保证其waitstatus不会被取
消。这里的描述在后面的方法也可以体现出来
tail:等待队列的尾部,已延迟初始化。仅通过方法enq修改以添加新的等待节点
state:同步状态的值。在不同的实现里面表现出来的是不一样的,个人这样认为。
-----------------------------------------------
从这看出来了这个通过头和尾确定了这个队列的,接续往下看这个操作队列的方法,既然队列中的节点包含了对应的线程那么肯定有执行这
个线程的地方。
插入节点的操作
-----------------------------
/**
* 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;
}
}
}
}
-----------------------------
这里就是基本超如操作,但是注意<CAS>操作保证了原子操作,关于<原子操作>和<CAS>,还有<volitale>,<synchronized>,缓存,内存
这一块的内容可以深入探究
为当前线程和给定模式创建节点并使其排队
-----------------------------
/**
* 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;
}
在共享可中断模式下获取。简单的来说就是将当前线程和节点绑定并添加到队列中
-----------------------------
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
-----------------------------
注意这里的tryAcquireShared是需要我们自己重写的
以共享定时模式获取。这里对等待的队列加上了一个时间的概念
-----------------------------
/**
* Acquires in shared timed mode.
*
* @param arg the acquire argument
* @param nanosTimeout max wait time
* @return {@code true} if acquired
*/
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
-----------------------------
注意这里的tryAcquireShared是需要我们自己重写的
我们看到了插入队列的操作那么对应的就有移除队列以及唤醒被移除的节点的线程
/**
* 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;
}
-------------------------------------------
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);这里就是唤醒节点的后续节点
这里doAcquireSharedInterruptibly我们看到有一个不跳出的循环咱满足条件的时候才跳出,即可以卡住线程,这个在countDownLaunch中
使用了。
大体的思路就是将当前线程以节点的形式保存下来,然后以某种方式将改节点的线程卡住,然后要打开这个线程就将这个卡住的方式解开,继续线程继续,这里我们就可以依据此方式扩展自己同步队列的方式。
我们思考下线程同步和一部主要针对的内容是什么呢,主要还是看自己的业务需求,也就是什么样子的一个顺讯,那些操作对顺序有要求,那些操作对顺序没有要求,那么我们只需要控制这个过程就可以了。
从源码中我们发现了两种模式 独占模式和共享模式,从名称上来看就很清楚了,独占是指仅一个线程占有其他的线程无法获取,而共享模式呢是指可以有其他线程获取但是不是一定能获取成功。
三:现有的基于aqs的同步器
结合享有的同步器,简单介绍如何实现的
1:Countdownlatch
package com.huo;
import java.util.concurrent.CountDownLatch;
public class TestCountdownlatch {
public CountDownLatch countDownLatch = new CountDownLatch(2);
public void test1() throws InterruptedException
{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1: "+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2: "+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
countDownLatch.countDown();
}
}).start();
System.out.println("3: "+System.currentTimeMillis());
countDownLatch.await();
System.out.println("4: "+System.currentTimeMillis());
}
public static void main(String[] args) throws InterruptedException {
new TestCountdownlatch().test1();
}
}
结果
1: 1562504507325
2: 1562504507326
3: 1562504507325
4: 1562504508326
--------------------------------结果及过程分析
可以很明显看到在await的时候当前线程被卡住了,在countDown执行完后,当前线程被释放继续运行。
注意CountDownLatch初始化的传入的参数为2,这个2代表的就是ASQ中的state,我们来看下countDown做了什么事,
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
可以看出来的是做了减一的操作,再来看下await做了什么事,
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们可以看出来的是,他将当前线程加入到Node节点,注意这里的r值不是指state的值,而是判断state的值是否等于0,然后返回1或者-1,在
countdownLaunch这里仅是将线程和Node绑定。线程的释放是在每次countDown的时候,直到state的值等于0的时候在执行释放操作。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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;
}
}
2:Semaphore
package com.huo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
public class TestSemaphore {
public Semaphore semaphore = new Semaphore(2);
public void test1() throws InterruptedException
{
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("1: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
semaphore.release();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("2: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
semaphore.release();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("3: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
semaphore.release();
}
}).start();
semaphore.acquire();
System.out.println("4: "+System.currentTimeMillis());
semaphore.acquire();
System.out.println("5: "+System.currentTimeMillis());
semaphore.acquire();
System.out.println("6: "+System.currentTimeMillis());
semaphore.release();
}
public static void main(String[] args) throws InterruptedException {
new TestSemaphore().test1();
}
}
结果
1: 1562506513793
2: 1562506513794
4: 1562506518794
3: 1562506518794
5: 1562506523794
程序未结束
---------------------结果及过程分析
这里的道理同Countdownlatch一样,这里就不细说了,唯一区别是state值是可以进行增加的也就是对应的release方法,acquire对state值做
减法,release对state值做加法。 4和5没有进行加法操作导致6未执行,可以看出来。
3:CyclicBarrier
package com.huo;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class TestCyclicBarrier {
public CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public void test1() throws InterruptedException
{
new Thread(new Runnable() {
@Override
public void run() {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("1: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("2: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("3: "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
public static void main(String[] args) throws InterruptedException {
new TestCyclicBarrier().test1();
}
}
结果
1: 1562509251860
2: 1562509251860
程序未结束
-----------------结果及过程分析
结果显示执行了1、2, 而3未执行,可以了解CyclicBarrier是卡住一定量的线程才执行被卡住的所有线程。
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
可以看到当index的值等于0的时候也就是被卡住线程正好等于传入的count值,然后执行对应的线程,再将count值还原(在构造函数的时候保存了一
个parties等于count值)。这里的线程保留是通过ReentrantLock进行的,还是通过Node的方式进行的。具体的代码源码可以升入,这里不做描述。
比较好的文章链接
The java.util.concurrent Synchronizer Framework
时间记录:2019-7-7