并发编程读后感 & AQS 初探

拖了很久,总算来简单总结下看完并发编程的感想

书籍读后感

最近看完了《Java 并发编程实战》,是 Brain Goetz 大神写的,里面描述了 JUC 并发包的核心知识点,了解到多线程下状态资源管理的本质,都是为了程序计算资源的高效利用,以及如何确保程序正确处理,保证线程的正确性和安全性!

摘自书中的描述

本书分为四个部分:

  • 基础知识:

    前面几章,都在介绍线程安全性的重要性,【对象 Object】如何正确的分享,以及 JUC 包下每个工具类的作用和使用场景,总结了常见和实用的规则,所以可以细细品味,加深基础

  • 结构化并发应用程序:

    从第二部分开始,开始介绍多线程下,如何让程序数据正确处理,例如多线程下,如何让一个行为表现正确,如何正确处理中断行为,介绍 volatile、内存模型等等,让我们了解 jvm 和操作系统是如何处理。

    同时也介绍了线程池 Executors,如何评估线程池的大小配置(根据 CPU 计算资源以及 IO 密集型,由公式计算得出),还有每个参数的含义,以及线程各种状态,释放锁与否等等。第八章【线程池的使用】,值得多次观看。

  • 活跃性、性能与测试:

    这部分其实阅读起来挺无趣的(个人感觉)。

    虽然我们都知道多线程的好处,但太过活跃,资源竞争更加强烈,更可能出现多个锁互相等待,造成死锁 Dead Lock,所以滥用多线程有可能造成服务器不良影响。

    于是需要进行测试,诊断死锁可能出现的情况,进行避免。同时在 11 章【性能与可伸缩性】,讨论了一个亘古不变的话题:运行正确以及运行更快。我们当然希望在同样服务器配置下,程序运行能够更快,但前提是程序要正确,所以要警戒自己:

    避免不成熟的优化。首先使程序正确,然后再提高运行速度-如果它还运行得不够快。

    所以,在阅读时,看到很多测试数据,虽然不是很有感觉,但对其中道理很是受用。

  • 扩展主题:

    第四部分说的是显示锁 Lock、原子变量、非阻塞算法以及开发自定义的同步工具类,将前面部分内容看完后,能够对其中的 CAS 操作,队列数据结构感到熟悉,更加感觉 JUC 开发者团队对操作系统的熟悉,编写出如此优秀的资源管理框架。

其中一些核心知识点和笔记:

前面四部分内容,做了一个简单的小介绍,我不觉得看过一次后能够完全掌握它,所以本次先做个初步学习,并且挑了一个最感兴趣的 知识点 AQS 去深入学习,学习大佬底层框架的设计理念。

AQS 初探

AQS 全称是 Abstract Queue Synchronizer,抽象队列同步器,是 JUC 包下,众多同步工具类的父类,基于 AQS 的扩展使用。

AQS 定义了状态依赖管理,查看了 ReentrantLock 可重入锁,底层的公平锁 FairSync 和非公平锁 NonfairSync,都是间接扩展自 AbstractQueuedSynchronizer,底层的加锁 lock 调用了 AQSacquire(1),解锁方法 unlock() 调用了 AQSrelease(1),所以可以来好好看看这个同步器的原理实现。

理解流程:

公共变量 state,是一个状态变量,状态管理的对象就是 volatile int state,后续相关操作都是对它更新来达到资源争夺的目的。

lock -> tryAcquire -> addWaiter -> acquireQueue -> selfInterrupt

数据结构:

既然是实现了 Lock 接口,根据语义,多个线程操作时,例如线程 A 抢到资源,正在处理中,那么线程 B 就无法获取相同的资源,这时候需要放置一个地方进行等待,于是就有了 CLH 同步队列(从注释中看出,这是三个人名)

1
2
3
4
5
* <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
* Hagersten) lock queue. CLH locks are normally used for
* spinlocks. We instead use them for blocking synchronizers, but
* use the same basic tactic of holding some of the control
* information about a thread in the predecessor of its node.

CLH 锁通常用于自旋锁。我们使用它们来实现阻塞同步器,但使用持有某些控件的相同基本策略,在前置节点 prev 中来保存有关其节点的的线程信息。

1
2
3
4
5
6
7
* <p>To enqueue into a CLH lock, you atomically splice it in as new
* tail. To dequeue, you just set the head field.
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>

在资源争夺失败情况下,会进入等待队列,CLH 是一个双向队列,该数据结构有前节点指针 pre 和后节点指针 next,指向对应的节点 Node

有关等待队列的详细实现,可以查看 AbstractQueuedSynchronizer.java 实现~

非公平锁加锁实例

1
2
3
4
5
6
7
final void lock() {
// CAS操作,抢占公共变量:状态 state
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

整理了一下非公平锁实现的 lock 时序图:

对比了公平锁 FairLock,两者的区别在于,非公平锁有两次机会直接抢占资源,不按照队列的 FIFO 顺序等待资源,而公平锁将会严格按照 FIFO 顺序来争夺锁资源:

配合时序图和代码实现,可以了解到同步资源管理器的强大,底层框架封装好了,并发工具包下,ReentrantLockSemaphoreCountDownLatch 等都是基于 AQS 扩展的,在多线程同时处理情况下,保证共享资源操作的正确性。

小结

从本次学习总结中,看完书籍后,对于多线程的作用和线程安全性有了更新的了解,想要深入往下扒 AQS 的实现原理却遇到了难处。

一方面是使用的比较少,另一方面看出我对操作系统底层和数据结构的确有很多不熟悉的知识点,例如 ReentrantLock 的分层设计、等待队列的实现细节,失败处理、中断处理,还有 unsafe 包下与操作系统的操作,都是后续要加深学习的知识点。

推荐美团技术写的 AQS 实现原理,看完之后获益匪浅,真正窥见 Doug Lea 大神们写的框架厉害,对于 CPU 时间、状态资源的利用有着高深理解,也是需要多看几遍加深理解!

所以这次就简单总结加锁流程以及查看源码和文章,加深了对 AQS 的理解,接下来得去好好了解操作系统的知识,夯实底层基础~


参考资料

1、《Java 并发编程实战》Brain Goetz
2、从ReentrantLock的实现看AQS的原理及应用