java.util.concurrent.locks.AbstractQueuedSynchronizer是J.U.C里最核心,也是最复杂的一个基础的类,简称AQS。AbstractQueuedSynchronizer是CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基础,因此AbstractQueuedSynchronizer是Lock/Executor实现的前提。
通过以下三个维度来实现:
第一个维度:原子性操作同步器的状态位
这里使用一个32位的整数来描述状态位,(AbstractQueuedSynchronizer类中有个属性叫:private volatile int state;
),在这里依然使用CAS操作来解决这个问题。事实上这里还有一个64位版本的同步器(AbstractQueuedLongSynchronizer,里面是long state)
第二个维度:阻塞和唤醒线程
JDK1.5之前使用Thread.suspend和Thread.resume实现,属于过时的api了,也有可能发生死锁。Jdk1.5之后使用LockSupport类来实现。AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()实现线程的阻塞和唤醒 的。提供的方法比如:
LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object,long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object,long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)
LockSupport是JDK中比较底层的类,是用来创建锁和其他同步工具类的基本线程阻塞原语。LockSupport 很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继 续 执行;如果许可已经被占用,当前线 程阻塞,等待获取许可。
public static void main(String[] args)
{
LockSupport.park();
System.out.println("block.");
}
运行该代码,可以发现主线程一直处于阻塞状态。因为 许可默认是被占用的 ,调用park()时获取不到许可,所以进入阻塞状态。
如下代码:先释放许可,再获取许可,主线程能够正常终止。LockSupport许可的获取和释放,一般来说是对应的,如果多次unpark,只有一次park也不会出现什么问题,结果是许可处于可用状态。
public static void main(String[] args)
{
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);//释放许可
LockSupport.park();// 获取许可
System.out.println("b");
}
LockSupport是可不重入 的,如果一个线程连续2次调用 LockSupport .park(),那么该线程一定会一直阻塞下去。
public static void main(String[] args) throws Exception
{
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);
System.out.println("a");
LockSupport.park();
System.out.println("b");
LockSupport.park();
System.out.println("c");
}
这段代码打印出a和b,不会打印c,因为第二次调用park的时候,线程无法获取许可出现死锁。
LockSupport可以响应中断性
public static void t2() throws Exception
{
Thread t = new Thread(new Runnable()
{
private int count = 0;
@Override
public void run()
{
long start = System.currentTimeMillis();
long end = 0;
while ((end - start) <= 1000)
{
count++;
end = System.currentTimeMillis();
}
System.out.println("after 1 second.count=" + count);
//等待或许许可
LockSupport.park();
System.out.println("thread over." + Thread.currentThread().isInterrupted());
}
});
t.start();
Thread.sleep(2000);
// 中断线程
t.interrupt();
System.out.println("main over");
}
最终线程会打印出thread over.true。这说明 线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException
第三个维度:一个有序的队列
在AQS中采用CHL列表来解决有序的队列的问题。
AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。
对于入队列(enqueue):采用CAS操作,每次比较尾结点是否一致,然后插入的到尾结点中。
do {
pred = tail;
}while ( !compareAndSet(pred,tail,node) );
对于出队列(dequeue):由于每一个节点也缓存了一个状态,决定是否出队列,因此当不满足条件时就需要自旋等待,一旦满足条件就将头结点设置为下一个节点。
while (pred.status != RELEASED) ;
head = node;
实际上这里自旋等待也是使用LockSupport.park()来实现的。
AQS里面有三个核心字段:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。head/tail加上CAS操作就构成了一个CHL的FIFO队列。下面是Node节点的属性。
volatile int waitStatus; 节点的等待状态,一个节点可能位于以下几种状态:
· CANCELLED = 1: 节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。
· SIGNAL = -1: 节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。
· CONDITION = -2:表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
· 0: 正常状态,新生的非CONDITION节点都是此状态。
· 非负值标识节点不需要被通知(唤醒)。
volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。
volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。
volatile Thread thread;节点绑定的线程。
Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。
CLH算法实现
CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。结点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个结点。CLHLock的类图如下所示:
当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁,然后线程对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁。当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点。如下图所示,线程A需要获取锁,其myNode域为true,些时tail指向线程A的结点,然后线程B也加入到线程A后面,tail指向线程B的结点。然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false,它就可以获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。
整个CLH的代码如下,其中用到了ThreadLocal类,将QNode绑定到每一个线程上,同时用到了AtomicReference,对尾指针的修改正是调用它的getAndSet()操作来实现的,它能够保证以原子方式更新对象引用。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private boolean isLocked = true; // 默认是在等待锁
}
@SuppressWarnings("unused" )
private volatile CLHNode tail ;
privatestatic final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER =AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class,CLHNode .class , "tail" );
public void lock(CLHNode currentThreadCLHNode) {
CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode); // 转载人注释: 把this里的"tail" 值设置成currentThreadCLHNode
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThreadCLHNode) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
// 还有后续线程
currentThreadCLHNode. isLocked = false ;// 改变状态,让后续线程结束自旋
}
}
}
针对CLS锁,后期优化出现了MCS锁。MCSSpinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
CLH锁与 MCS锁的比较
下图是CLH锁和MCS锁队列图示:
差异:
- 从代码实现来看,CLH比MCS要简单得多。
- 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋
- 从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
- CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
注意:这里实现的锁都是独占的,且不能重入的。
下面从Semaphore 和CountDownLatch源码实现来说明AQS:
① Semaphore
semaphore代码执行的流程,分析acquire的过程
信号量在多线程中有着重要的应用,它的原理是将资源抽象成信号量,如果信号量大于0表明有可用资源,小于0,则需要等待。
对应申请时,就是做信号量减操作;
对应释放时,就是做信号量加操作。
资源又抽象成state,这是个所有线程都可见的变量。通过state就可以判断线程是不是可以访问共享资源了。
1.new Semaphore()对象时,它调用的 sync = new NonfairSync(permits);可以看出它是调用非公平的同步辅助类,其中sync的类型是Sync,它的定义如下:
abstract static class Sync extendsAbstractQueuedSynchronizer{};
2.NonfairSync类又继承Sync类,它具体实现如下:
final static class NonfairSync extends Sync{
private static finallong serialVersionUID = -2694183684443567898L;
NonfairSync(intpermits) {
super(permits);
/*此处调用的是Sync(intpermits) {
setState(permits);
}
最终state的定义是这样的private volatile int state;
这个state对所有的线程是可见的,并且是最新的值。
*/
}
//这个是在第三步中的acquireSharedInterruptibly()方法被调用
protected inttryAcquireShared(int acquires) {
returnnonfairTryAcquireShared(acquires);
}
}
3.看看acquire()方法的执行过程
先是调用如下的方法:
public void acquire() throwsInterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquireSharedInterruptibly(1) 这个方法调用下面这个方法:
public final voidacquireSharedInterruptibly(int arg) throws InterruptedException {
if(Thread.interrupted())
thrownew InterruptedException();
//如果小于0,表明没有资源了,就要进入队列中去
if (tryAcquireShared(arg)< 0)
doAcquireSharedInterruptibly(arg);
}
4.tryAcquireShared()方法调用nonfairTryAcquireShared()方法,看看它的具体是怎样实现。
final int nonfairTryAcquireShared(intacquires) {
for (;;){
//最新的state值
int available = getState();
//还剩下多少个许可证了(permit)
int remaining = available - acquires;
//只有在还剩有时才更新状态
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
5.如果没有许可证了,就要进入队列中去了,看看doAcquireSharedInterruptibly()这个方法的实现
private voiddoAcquireSharedInterruptibly(int arg)
throwsInterruptedException {
final Node node =addWaiter(Node.SHARED);
/*
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;
}
*/
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
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
break;
}
} catch (RuntimeExceptionex) {
cancelAcquire(node);
throw ex;
}
// Arrive here only ifinterrupted
cancelAcquire(node);
throw newInterruptedException();
}
② CountDownLatch
CountDownLatch源代码分析
1.首先分析它的功能:Asynchronization aid that allows one or more threads to wait until a set ofoperations being performed in other threads completes.
也就是说主线程在等待所有其它的子线程完成后再往下执行。常见的例子是待所有的运动员跑完后再排名。
2.它所提供的方法如下:
构造函数:CountDownLatch(intcount)//初始化count数目的同步计数器,只有当同步计数器为0,主线程才会向下执行
主要方法:void await()//当前线程等待计数器为0
boolean await(long timeout,TimeUnit unit)//与上面的方法不同,它加了一个时间限制。
void countDown()//计数器减1
long getCount()//获取计数器的值
3.它的内部有一个辅助的内部类:sync.
它的实现如下:privatestatic final class Sync extends AbstractQueuedSynchronizer {
private static finallong serialVersionUID = 4982264981922014374L;
/*此处在new CountDownLatch时会调用:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}*/
Sync(int count) {
setState(count);
}
int getCount() {
returngetState();
}
protected inttryAcquireShared(int acquires) {
return(getState() == 0) ? 1 : -1;
}
protected booleantryReleaseShared(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;
}
}
}
4.await()方法的实现
sync.acquireSharedInterruptibly(1);
-->if(tryAcquireShared(arg) < 0)//调用3中的tryAcquireShared()方法
doAcquireSharedInterruptibly(arg);//加入到等待队列中
5.countDown()方法的实现
sync.releaseShared(1);
--> if(tryReleaseShared(arg))//调用3中的tryReleaseShared()方法
doReleaseShared();//解锁