深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)
深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(五):条件队列(Condition)
那些“简单的”并发代码背后,隐藏着大量信息。。。
独占锁虽说在j.u.c中有现成的实现,但在JAVA的语言层面也同样提供了支持(synchronized);但共享锁却是只存在于AQS中,而它在实际生产中的使用频次丝毫不亚于独占锁,在整个AQS体系中占有举重若轻的地位。而在某种意义上,因为可能同时存在多个线程的并发,它的复杂度要高于独占锁。本章除了介绍共享锁数据结构等,还会重点对焦并发处理,看 doug lea 在并发部分是否有遗漏
j.u.c下支持的并发锁有Semaphore、CountDownLatch等,本章我们采用经典并发类Semaphore来阐述
二、简介共享锁其实是相对独占锁而言的,涉及到共享锁就要聊到并发度,即同一时刻最多允许同时执行线程的数量。上图所述的并发度为3,即在同一时刻,最多可有3个人在同时过河。
但共享锁的并发度也可以设置为1,此时它可以看作是一个特殊的独占锁
2.1、waitStatus在独占锁章节中,我们介绍到了关键的状态标记字段waitStatus,它在独占锁的取值有
0
SIGNAL (-1)
CANCELLED (1)
而这些取值在共享锁中也都存在,含义也保持一致,而除了上述这3个取值外,共享锁还额外引入了新的取值:
PROPAGATE (-3)
且-3这个取值在整个AQS体系中,只存在于共享锁中,它的存在是为了更好的解决并发问题,我们将在后文中详细介绍
2.2、使用场景本人参加的某性能挑战赛中,有这样一个场景:数据产生于CPU,且有12个线程在不断的制造数据,而这些数据需要持久化到磁盘中,由于数据产生的非常快,此时的瓶颈卡在IO上;磁盘的性能经过基准测试,发现每次写入8K数据,且开4个线程写入时,能将IO打满;但如何控制在同一时刻,最多有4个线程进行IO写入呢?
其实这是一个典型的使用共享锁的场景,我们用三四行代码即可解决
// 设置共享锁的并发度为4 Semaphore semaphore = new Semaphore(4); // 加锁 semaphore.acquire(); // 执行数据存储 storeIO(); // 释放锁 semaphore.release(); 三、并发 3.1、独占锁 vs 共享锁共享锁的整体流程与独占锁相似,都是首先尝试去获取资源(子类逻辑,一般是CAS操作)
如果能拿到资源,那么进入同步块执行业务代码;当同步块执行完毕后,唤醒阻塞队列的头结点
如果资源已空,那么进入阻塞队列并挂起,等待被其他线程唤醒
两者的不同点在什么地方呢?就在于“唤醒阻塞队列的头结点”的操作。在独占锁时,唤醒头结点的操作,只会有一个线程(加锁成功的线程调用release())去触发;而在共享锁时,可能会有多个线程同时去调用释放
直观感觉这样设计不太合理:如果多个线程同时去唤醒头结点,而头结点只能被唤醒一次,假定阻塞队列中有20个节点,那这些节点只能等待上一个节点执行完毕后才会被唤醒,无形中共享锁的并发度变成了1。要解决这个疑问,我们先来看共享锁的释放逻辑
3.2、锁释放先来思考一下锁释放需要做的事儿
1、阻塞队列的第一个节点一定要被激活;这个问题看似不值一提,却相当重要,区别于独占锁,共享锁的锁释放是存在并发的,在高并发的流量下,一定要保证阻塞队列的第一个有效节点被激活,否则会导致阻塞队列永久性的挂死