Java并发编程系列文章
欢迎大家观看我的博客,会不断的修正和更新文章,也欢迎大家一起交流
- Java并发编程系列 | 原子操作的底层原理
- Java并发编程系列 | volatile关键字的底层原理
- Java并发编程系列 | synchronized的锁升级过程
- Java并发编程系列 | 线程池原理详解
- Java并发编程系列 | FutureTask原理详解
- Java并发编程系列 | AQS之ReentrantLock原理详解
AQS之条件队列的原理
AQS是锁和一些同步器实现的基础组件,它提供了一个阻塞队列,并且实现了入队列和出队列和信号通知下一个节点,以及其他队列操作
相关的方法,同时还有state,tryAcquire是由子类去自己定义和实现的
- 在ReentrantLock 中,state表示当前线程获取锁的重入次数,在ReentrantReadWriteLock中,state高16
位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的重入次数,
在semaphore中,state表示当前可用信号的数量,在CountDownLatch中,state表示计数器当前的值,然后他们根据自己
不同的特点来实现tryAcquire方法 - 在AQS中的阻塞队列中,Node节点的waitStatus总共有五种状态
1.CANCELLED
线程被中断,或者超时了,就会在cancelAcquire里将waitStatus置为CANCELLED
2.SIGNAL
表示现在Node正在AQS队列等待,需要unpark来唤醒
3.CONDITION
表示现在Node正在条件队列里等待
4.PROPAGATE
表示释放共享资源时需要通知其他节点
5.0
初始值,不代表以上4个任何一种状态 条件队列的实现
当使用reentrantLock时,在lock和unlock中使用condition.await()和condition.signal()时,其实就像是在synchronized中使用object.wait()和object.nofity()一样,都是起到一个阻塞和通知的作用,不过使用reentrantLock所不同的是,可以使用多个条件变量,来在lock和unlock之间进行阻塞和通知的,而synchronized只能在代码块中使用被synchronized修饰的那个变量来进行wait和notify。
- await的流程
当使用await()方法时,先生成一个当前节点添加到到条件队列,再把state置为0,取消线程独占并释放锁,接下来park等待signal执行且把节点重新放回AQS队列中,然后再尝试在队列里等待unlock通知的这样的一个流程。
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter();//生成一个当前节点添加到到条件队列 int savedState = fullyRelease(node);//把state置为0,取消线程独占并释放锁 int interruptMode = 0; while (!isOnSyncQueue(node)) { //如果这个node还没有出现在AQS阻塞队列里面,意思就是还在条件队列中 //就会park阻塞当前线程 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //到这来的时候,就表示这个条件已经被signal并且又被放入阻塞队列,且又被通知了 //这里就会恢复state的状态为原来savedState这么多 //并等待被前面的节点unpark通知或者打断 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled //如果条件队列中有节点已经超时或者被取消了,在这里就会被清理掉 unlinkCancelledWaiters(); if (interruptMode != 0) //如果是被打断的,就会在这里抛出InterruptedException reportInterruptAfterWait(interruptMode); }
- signal的流程
注意,signal的流程,signal除了被取消和通知的会unpark它的线程外,就不会unpark执行通知了,所以在这里,signal的作用其实就是将节点从条件队列移出和往AQS队列中加入节点。
所以我刚开始还有一点疑问的就是,这个signal一般情况下不执行unpark来通知,那await代码里的LockSupport.park(this),到底是什么时候被谁通知的?
后来调了调代码,发现原来是在signal之后,执行unlock时通知的,因为执行unlock后又会自动的去AQS队列里面找节点来通知,而signal就把await放到条件队列里的节点再放回AQS队列,这样通知的时候就也是通知await的线程了。
public final void signal() { //如果该锁不是被这个线程独占的,就抛出异常 //这就决定了signal必须要在reentrantLock的lock和unlock之间使用 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) //将条件队列中的第一个节点出队列,并将其放入AQS队列 doSignal(first); }
private void doSignal(Node first) { do {//将firstWaiter 指针指向下一个节点, //并让原来firstWaiter的下一个节点置为null,相当于把该节点移除了 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }
final boolean transferForSignal(Node node) { //使用原子操作将waitStatus设置为0,如果没设置成功,那就是被取消了,直接返回false if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //将节点插入AQS队列 Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) //如果说这个节点被取消了,就直接通知 //节点没有被取消的话,还会将waitStatus状态设置为SIGNAL LockSupport.unpark(node.thread); return true; }
一个简单的流程图
一个小问题
为什么像object.wait()和notify(),以及condition.await()和signal(),他们为什么必须要放在自己的同步代码块中,否则就会报错呢?是出于什么目的这样设计的呢?
- 其实就是这一个目的:这是为了保证其并发安全性
- 就以ArrayBlockingQueue来举例
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } } /** * Inserts element at current put position, advances, and signals. * Call only when holding lock. */ private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++;//设置条件 notEmpty.signal(); } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0)//判断条件 notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
- 我们就依据notEmpty来分析,首先可以直接在代码上看到,notEmpty是一直处于lock和unlock代码块中的,并且enqueue上都有注释提示了我们。
- 一般来说,使用condition的话,都会有带着一定的条件变量使用的,并且条件的判断和赋值都是在同步块中的,这种设计在java并发包中随处可见,而在这里,notEmpty就使用了count来进行判断。
- 如果说,await()和signal()可以不用放在同步块中,那么,就会产生并发的问题
时间节点1 时间节点2 时间节点3 时间节点4 最终结果 线程A 在take中判断count为0 执行notEmpty.await() 会一直阻塞 线程B 在enqueue中count++ 执行notEmpty.signal()通知 时间节点1 时间节点2 时间节点3 时间节点4 最终结果 线程A 在take中判断count为1 不执行notEmpty.await() 取数据 线程B 在take中判断count为1 不执行notEmpty.await() 取数据 线程C 在enqueue中count++ 执行notEmpty.signal()通知 - 上面这两种情况都是严重的并发错误,必须是要将他们同步起来的,所以说,object.wait()和notify(),以及condition.await()和signal(),他们必须要放在自己的同步代码块中。
总结
条件队列也是非常重要的一个知识点,它类似于object.wait()但是却可以支持多个条件,在java的并发包里也有大量的使用到,比如一些信号量的底层实现,还有所有的BlockingQueue这些,它有notEmpty和notFull这两个条件,用来控制存和取时的阻塞,像这样的要支持多个条件的,object.wait()它就做不到。但是如果想要了解BlockingQueue的原理,但你不了解条件队列,那么阅读它的源码时也会有各种困难。
- await的流程