前提
在阅读本文之前,建议先阅读我的《初步了解AQS是什么(一)》 ,毕竟有一些内容是和前文是相通的,如果对AQS熟悉的话,也可以直接往下看,不过还是先建议先看下
公平锁和非公平锁
ReentrantLock默认使用的是非公平锁,除非在构造方法里面传入true就可以变为公平锁啦!
1 2 3 4 5 6 7
public () { sync = new NonfairSync(); } public (boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
先看看公平锁的lock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
final void lock () { acquire(1 ); } public final void acquire (int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire (int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0 ) { if (!hasQueuedPredecessors() && compareAndSetState(0 , acquires)) { setExclusiveOwnerThread(current); return true ; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0 ) throw new Error("Maximum lock count exceeded" ); setState(nextc); return true ; } return false ; }
非公平锁的lock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
final void lock () { if (compareAndSetState(0 , 1 )) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1 ); } public final void acquire (int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire (int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire (int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0 ) { if (compareAndSetState(0 , acquires)) { setExclusiveOwnerThread(current); return true ; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0 ) throw new Error("Maximum lock count exceeded" ); setState(nextc); return true ; } return false ; }
总结:
非公平锁在lock的时候,在tryAcquire函数里面会CAS获取state,如果恰好成功,那么就直接取得锁啦,不用进行下面的操作了
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
非公平锁如果两次CAS 都不成功,那么接下来的操作和公平锁一样啦!都要进入到阻塞队列等待唤醒。
非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
生产者消费者模式
在读下面的内容的时候,让我们来先了解一下生产者和消费者的模式,主要有个例子,更好地理解下面的内容!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100 ]; int putptr, takeptr, count; public void put (Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0 ; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take () throws InterruptedException { lock.lock(); try { while (count == 0 ) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0 ; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
上面只是贴个代码,先让读者知道怎么用ReentrantLock和Condition,关于更多的细节,请读者自行百度即可。
ConditionObject
Condition接口的实现类。Condition的方法依赖于ReentrantLock,而ReentrantLock又依赖于AbstractQueueSynchronized类。
内部结构
1 2 3 4 5
private static final long serialVersionUID = 1173984872572414699L ; private transient Node firstWaiter; private transient Node lastWaiter;
条件队列
在AQS中,其中线程等待的队列我们成为阻塞队列,这里先引入条件队列(condition queue),这里先上图看两者的关系。
区别
阻塞队列和条件队列都是node节点,也就是Node的实例。因为条件队列的节点是需要转移到阻塞队列的。
ReentrantLock是可以创建多个Condition实例的,则这里会有condition1和condition2,并且ConditionObject中只有两个和节点有关的属性 firstWaiter和nextWaiter
每个Condition都有一个条件队列与之关联。当调用await()方法的时候,当先线程则会阻塞在当前地方,不会往下执行,然后将当前线程封装成Node节点,添加到条件队列的队尾。
调用Condition.signal()会触发唤醒。但是唤醒的是队头节点。也就是将对应线程的条件队列的对头(firstWaiter指向的节点)移到阻塞队列的队尾,等待获取锁。获取锁之后,await才会返回,然后继续往下执行。
源码分析
await:让线程挂起等待,并交出锁
await():是可以被中断的,调用这个方法的线程会阻塞,直到调用Signal()或者被中断。
awaitUninterruptibly():不可被中断,就是说有中断信号来了但不会响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
public final void await () throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0 ; while (!isOnSyncQueue(node)) { LockSupport.park(this ); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0 ) break ; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null ) unlinkCancelledWaiters(); if (interruptMode != 0 ) reportInterruptAfterWait(interruptMode); }
addConditionWaiter:将条件队列的节点加入阻塞队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
private Node addConditionWaiter () { Node t = lastWaiter; if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null ) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
在addConditionWaiter中,有一个unlinkCancelledWaiters()方法,顾名思义就是取消不等待的节点,因为条件队列是单向链表,所以涉及到链表的操作。
就是说如果在await的时候,节点取消等待或者是说节点入队的时候,发现最后一个节点已经取消了,那么就调用一次这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
private void unlinkCancelledWaiters () { Node t = firstWaiter; Node trail = null ; while (t != null ) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null ; if (trail == null ) firstWaiter = next; else trail.nextWaiter = next; if (next == null ) lastWaiter = trail; } else trail = t; t = next; } }
fullyRelease: 完全释放独占锁
在await方法中,把节点加入到条件队列之后,就需要完全释放锁了。因为考虑到重入的问题,所以必须要完全释放。
例子:condition.await之前,当前节点就已经执行了两次lock()操作,那么state为2,也就是说拥有两把锁。那么fullyRelease就应该设置state为0,返回2,返回没释放锁之前所拥有的锁的数量。然后再进行挂起。当被唤醒的时候,依旧需要两把锁才能进行执行下去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
final int fullyRelease (Node node) { boolean failed = true ; try { int savedState = getState(); if (release(savedState)) { failed = false ; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
isOnSyncQueue():判断是否进入阻塞队列
在await方法中,完全释放锁之后,这个方法会判断该线程代表的队列是否在阻塞队列中,注意是阻塞队列 。
前面我们也说过,在节点加入阻塞队列的时候,会把ws 设置为Node.Condition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
final boolean isOnSyncQueue (Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null ) return false ; if (node.next != null ) return true ; return findNodeFromTail(node); } private boolean findNodeFromTail (Node node) Node t = tail; for (;;) { if (t == node) return true ; if (t == null ) return false ; t = t.prev; } }
如果isOnSynchronized返回false的话,那么就先挂起等待呗,也就是等待其他线程signal或者中断 唤醒线程,将这个线程加入阻塞队列!
signal:唤醒线程,让线程得回锁
前面说到线程挂起了,就等待着被唤醒呗!那么这里先说下signal函数,方便后面理解。
唤醒操作一般是由另外一个线程操作的,调用同一个对象的signal对象唤醒就好,其实唤醒的操作就是将节点从条件队列移动到阻塞队列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
public final void signal () { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null ) doSignal(first); } private void doSignal (Node first) { do { if ( (firstWaiter = first.nextWaiter) == null ) lastWaiter = null ; first.nextWaiter = null ; } while (!transferForSignal(first) && (first = firstWaiter) != null ); } final boolean transferForSignal (Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0 )) return false ; Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true ; }
检查中断状态
在signal之后,线程所代表的node节点肯定已经加入到阻塞队列中啦!然后就准备去获取锁。
之前不是说到线程已经挂起了吗?signal之后或者被中断就已经唤醒啦
1 2 3 4 5 6 7 8
int interruptMode = 0 ;while (!isOnSyncQueue(node)) { LockSupport.park(this ); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0 ) break ; }
有以下几种情况会让线程从LockSupport.park(this);这步往下走
signal之后进入阻塞队列,等待前驱节点释放锁,释放锁的时候就会调用 LockSupport.unPark(node.thread),也就是被唤醒啦!
当线程在park的时候,有其他线程对它进行了中断。
在signal的时候,进行过进队操作,但是前驱节点已经取消等待了或者CAS前驱节点的状态为SIGNAL也失败
假唤醒。这个也是存在的,和 Object.wait() 类似,都有这个问题
线程被唤醒之后,第一步就是执行checkInterruptWhileWaiting(node)这个函数啦,如果返回非0,就是说await的线程在park期间或者说signal之后(unpark之后)被中断过,所以就要区分到底是signal之前中断还是signal之后中断。
如果返回0,那么就是说线程在park的时候,没有被中断,被unpark就是在阻塞队列正常被唤醒的!
checkInterruptWhileWaiting返回的值
如果在signal之前已经进行中断,那么返回THROW_IE -1
如果在signal之后已经进行中断,那么返回REINTERRUPT 1
如果返回0,那么表示在park期间没有进行中断
1 2 3 4 5
private int checkInterruptWhileWaiting (Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0 ; }
如何区分signal之前中断还是之后中断
主要在transferAfterCancelledWait这个方法里,我们下面来看一下它的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
final boolean transferAfterCancelledWait (Node node) { if (compareAndSetWaitStatus(node, Node.CONDITION, 0 )) { enq(node); return true ; } while (!isOnSyncQueue(node)) Thread.yield(); return false ;
所以经过上面的分析,只要是某个线程被中断,那么不管这个线程所代表的node节点是不是firstWaiter,也就是是不是会被signal,都会被唤醒,然后进入阻塞队列。
获取独占锁
我们回到await函数,在上面的while退出之后,也就是我们的节点不管是被signal也好还是被中断也好,都已经进入到阻塞队列了,这点是毋庸置疑的。进入阻塞队列后干嘛呢?当然是为了获取锁啦,那么如何获取锁呢?还记得上一节我们讲的进入阻塞队列之后如何获取锁么?如果不记得就去看看吧!这里放出连接
1 2 3 4 5
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT;
1 2 3
if (node.nextWaiter != null ) unlinkCancelledWaiters();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
if (interruptMode != 0 ) reportInterruptAfterWait(interruptMode); private void reportInterruptAfterWait (int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt(); } static void selfInterrupt () { Thread.currentThread().interrupt(); }
AQS 独占锁的取消排队
下面我们来分析下,如何取消锁之间的竞争,当有几个线程想竞争某个锁的时候,我们希望有一个线程不去竞争这个锁了。
在第一篇文章中我们用的lock,如果有中断是不会抛出异常的,只是标记一下状态而已,具体的由外层函数决定。
在ReentrantLock中,有这样的一个lock方法,可以检测到中断并且抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
public void lockInterruptibly () throws InterruptedException { sync.acquireInterruptibly(1 ); } public final void acquireInterruptibly (int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } private void doAcquireInterruptibly (int arg) throws InterruptedException { 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 ; failed = false ; return ; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
上面我们看到,acquireInterruptibly里面的doAcquireInterruptibly在竞争锁的时候,一旦其他线程对其产生了中断,那么这个线程就马上结束,不再去竞争。如果是acquired的话,如果在竞争锁期间其他线程对其产生中断,那么先先抢得锁再说,中断后面再处理!这就是他们的区别啦!
总结
分析ReentrantLock公平锁和非公平锁的源码,理解其中的区别。
引入条件队列,并且和阻塞队列进行比较,分析他们的关系
对Condition的底层代码进行了部分的分析,主要是Node节点的await和signal的转换以及如何处理中断的问题
对AQS消除锁竞争的两种方式进行了源码分析,理解二者的不同
通过写这篇博客,对AQS的源码有了进一步的了解,本文主要花了大量的篇幅去写ConditionObject,主要是为了弄明白Node节点在条件队列和阻塞队列的转换的过程,同时自己由于之前对中断不是很熟悉,所以补了一些知识,起码比之前熟悉了一点,也算是一点小进步吧!后面会继续了解AQS,下次见!
近期评论