深入理解Java并发框架AQS系列(五):条件队列(Condition) (3)

Q:AQS测试中的“步骤二”,为什么在判断“等待10个线程全部进入wait方法”时,要引入lockedNum.get() != 10的判断?直接通过判断所有线程是否均为waiting方法不可以吗?

A:如果真的删除lockedNum.get() != 10的判断,在多次并发测试时,会有较小的概率出现程序死锁的情况(作者电脑的环境是平均5万次调用会出现一次),为什么会出现死锁呢?我们追AQS源码就会发现,不管是调用lock()还是await,挂起线程使用的方法均为LockSupport.park()方法,此方法会将线程置为WAITING状态,也就是线程状态是WAITING状态时,有可能线程刚进入lock()方法,从而导致await与thread.join()的死锁

Q:既然是这样,为什么JDK的测试没有出现死锁?

A:我们看到JDK的加锁是通过synchronized关键字完成的,而当线程因为等待synchronized资源而阻塞时,线程状态将变为BLOCKED,而进入wait()方法后,状态才会变为WAITING

Q:那看来只有通过引入AtomicInteger lockedNum变量才能解决死锁问题了

A:其实解决问题的方式有很多种,我们甚至可以简单将ReentrantLock lock置为公平锁,也能解决上述死锁问题;因为当前场景发生死锁的情况是,singalAll()先于await()发生,而当所有线程都变成WAITING状态后,公平锁则确保了singalAll()一定是在所有线程都调用了await()。但因为synchronized本身是非公平锁,故如果AQS使用公平锁的话,性能偏差较大

Q:那这样看来,AQS中的阻塞队列相对比JDK的没有优势可言啊,用法上没有JDK简洁,性能上还没人家快

A:的确,如果真是只是单纯的使用阻塞、唤醒功能的话,还是建议使用JDK内置的方式;但AQS的优势并不在此

五、再说AQS条件队列

AQS的优势在于,其提供了丰富的api可以查询条件队列的状态;例如当我们想看一下在条件队列中等待节点的个数时,使用JDK的wait/notify时,是无法做的;AQS提供的api如下:

boolean hasWaiters() 阻塞队列中是否有等待节点

int getWaitQueueLength() 获取阻塞队列长度

Collection<Thread> getWaitingThreads() 获取阻塞队列中线程对象

这些api为程序提供了更灵活的控制,条件队列对于javaer已不是黑盒;当然使用AQS的条件队列必然要引入独占锁,例如ReentrantLock,自然地我们还可以通过它查看条件队列外围的一些指标,例如:

Interrupted 响应中断,借助独占锁,提供响应中断能力; wait/notify不提供,因为虽然wait方法响应中断,但是synchronized关键字是会一直阻塞的

boolean tryLock() 尝试获取锁; wait/notify不提供

int getHoldCount() 获取阻塞线程的数量

boolean isLocked() 是否持有锁

fair/nonFair 提供公平/非公平锁

...

可见整个AQS体系相比较Object的wait/notify方法是相当灵活的,提供了很多监控条件队列、阻塞队列的指标

六、致谢

这里要特别感谢一下神策数据的架构师金满仓,同时也是我私下的挚友。他功力深厚,对程序有着自己独到的见地,在整个AQS编写期间,不厌其烦地给我提供了很多理论及数据上的支持,帮我拓宽视野,再次感谢!

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwjwdw.html