前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解Java并发同步框架AbstractQueuedSynchronizer的设计

理解Java并发同步框架AbstractQueuedSynchronizer的设计

作者头像
我是攻城师
发布2018-08-16 14:36:41
4520
发布2018-08-16 14:36:41
举报
文章被收录于专栏:我是攻城师我是攻城师

前言

Java并发包里面的大多数工具框架大部分都是基于AbstractQueuedSynchronizer(简称AQS后面不再区分)框架实现的,这个框架提供了通用的机制来自动管理同步状态,线程的阻塞加锁和解锁,及公平和非公平的线程队列等等,所以这个工具框架的重要性不言而喻,关于AQS的详细介绍建议大家读Doug Lea的关于AQS的论文。

关于AQS

在Java的并发包里面锁的表现形式有许多种,比如:

(1)互斥排它锁(mutex exclusion locks)

(2)读写锁(read write locks)

(3)信号量(semaphores)

(4)栅栏(barriers or countdownlatch)

(5)组塞获取结果 (future)

(6)队列 (queues)

上面这些是Java内置的工具包,其实他们都是基于AbstractQueuedSynchronizer实现的,如果上面的这些工具仍然不能满足我们的需要,那么我们就可以直接基于AbstractQueuedSynchronizer来构建任何我们想要的并发包,AQS提供了非常灵活的可扩展性,这正是这个框架的精华所在。

AQS的设计

(一)AQS的暴露两种类型的方法:

(1)acquire

至少有一个acquire相关的操作会阻塞调用该方法的线程,除非或者直到它的同步状态可用来允许该线程继续执行。

(2)release

至少有一个release相关的操作去改变同步状态,从而通知一个或者多个阻塞线程取消它们的阻塞状态。

事实上基于AQS构建的并发工具来都符合上面的规则,比如Lock.lock方法,Semaphore.acquire方法,CountDownLatch.await方法还有FutrueTask.get方法在底层都是映射到AQS的acquire方法,同理对应的解锁方法映射到AQS的release方法

(二)AQS提供的三种重要的能力

(1)提供了非阻塞同步的能力,通过类似的tryLock方法等

(2)提供了阻塞超时的能力,如果在指定时间内没有获取到锁,该线程就可以主动放弃加锁操作

(3)提供了可取消任务的能力,通过线程的打断操作。

(三)AQS提供了两种的加锁模式

(1)互斥排它锁,对于临界区的资源任何时候只能有一个线程操作。针对互斥锁AQS还支持Condition条件量用来细化管理对一个锁的await和singal操作。

(2)共享锁,对于临界区的资源,可以允许多个线程同时操作,典型的代表实现就是读写锁。

(四)AQS的性能和功能的核心之CLH队列

为了实现一个高效的公平管理的同步框架,这里需要一个FIFO队列来承担这个职责,当然既然是FIFO队列那么这里就不支持基于优先级的调度功能,在doug lea的论文中指出了针对这个队列的选择其实也是有争论的。当时主流的有两种设计队列,一种是CLH另一种是MCS,这两种队列的设计思想和实现我在前面的文章已经非常详细的介绍过,有不清楚的朋友可以再回顾一下。根据doug lea在论文中的表达,这里选择CLH队列的原因是因为CLH队列在处理任务取消和超时的情况下更加容易。

如果非得说AQS的核心是哪一个功能,那么改造版本的CLH队列绝对是AQS的灵魂,这也是为什么在类名字里面包含了队列这个单词。AQS提供了高吞吐和可扩展的能力,这与里面的使用这个队列有密切关系,CLH是基于自旋锁+CAS操作的公平的FIFO队列,通过了一个隐式的链表,让每个节点通过在前驱节点上自旋,因为消耗时间是常量时间,所以取得了较高的性能,。当然在AQS里面对原生的CLH是有改良的:

(1)添加了next节点,用来快速的定位它的后继节点,因为原生的CLH队列(单向链表),每个Node节点是自旋前驱节点来感知同步状态的变化,所以不需要next节点。但在一个阻塞的同步模式下当前节点需要显式的唤醒(使用LockSupport.unpark)其后继节点。所以这里其实是一个双向链表了。

(2)在每一个Node节点上通过一个status状态字段来控制阻塞而不是通过自旋,因为基于FIFO模式的对列,只有处于对列头部的节点才有资格获得临界区资源的操作,既然这样那么其他的非头部节点其实没必要一直不断的自旋前驱来感知状态变化,因为这里并没有大量的内存竞争在读取head节点。所以在head节点完事之后,可以通过next属性直接告诉后继你可以执行。当然关于status字段的另一个优化,在调用线程的park方法之前会重新检查同步状态,这样可以避免不必要的调度的开销,这对于临界区代码执行时间较短的场景是一个大的优化,如果临界区执行时间太长那么这里会阻塞挂起线程。

这里acquire和release的开销是O(1),但取消线程的开销最坏情况下是O(n),因为取消的线程不可能再次获取锁,所以有可能需要遍历整个队列来判断打断和超时。

(五)关于AQS的封装性

在AQS里面有三种比较重要组件:

(1)原子状态的管理

(2)阻塞和解锁

(3)队列的维护

AQS将这三个组件封装在一个类中主要原因是因为如果分开可能不高效且更加不易使用,因为状态会引起阻塞和解锁,而解锁和阻塞由影响队列的操作,所以这里有意限制了可应用的范围,但提供了更加高效的支持,因此实践中这里没有理由不使用AQS来构建或者管理同步工具。

AQS的公平性

尽管AQS的内部是基于CLH的FIFO的公平队列来管理线程状态,但这里并没有强制要求实现必须是公平的。

在CLH队列里面的head节点一定是第一个执行的吗?这个具体看实现,比如在上一个节点释放锁的瞬间,head节点准备加锁的试着加锁的时候,如果不要求公平性,那么新进来的线程是完全可以参与竞争的,如果竞争成功,那么新进来的线程就会抢占执行,另一个节点就会继续阻塞。当然这个功能我们可以控制,如果想要完全公平性,我们只需要在加锁的时候调用getFirstQueuedThread方法来判断当前的线程是不是对列的head节点,如果不是就进入加入队列尾部,否则就执行,这样一来就能保证公平的语义。

总结

本文主要介绍了Java里面AQS的相关知识和核心设计,了解这些核心的思想对于帮助我们理解它的源代码和其衍生的工具包会有很大帮助。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-08-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 关于AQS
  • AQS的设计
  • AQS的公平性
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档