AQS源码分析-以ReentrantLock为例(青铜)
AQS源码分析-以ReentrantLock为例(青铜)
前言
摘要
java实现线程同步最主要的2种方式是synchronized同步和工具锁,这两种同步机制在原理上有着较大的区别,在之前的一篇博客中,已经对synchronized原理做了简要描述,本文的主要目的是分析工具锁的原理。
java提供的工具锁主要有ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等,这些工具锁的实现都依赖于java提供的一个同步器框架AbstractQueuedSynchronizer,简称AQS。最近刚好学习了这部分的内容,感觉又很多东西值得总结,所以决定先从最核心的lock方法入手,争取对AQS有一个全面的认识。
本文要解决的问题
- 什么是AQS?AQS有什么作用?如何基于AQS实现自定义工具锁?
- ReentrantLock的非公平锁lock()方法源码过程是怎样的?核心步骤是怎样的?
- AQS的核心是什么?
- AQS中使用了什么设计模式?
正文
什么是AQS?
通过阅读JAVA API中,对AQS类的定义,主要的内容如下:
- Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
- 提供了一个基于队列的阻塞锁和响应的同步器
- This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state
- AQS可以非常好地支持大部分能够用一个原子整数表达状态的同步器。
- Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released。
- 实现AQS的子类,必须定义更改stat的方法,并且明确在获取锁或者释放锁时,stat对应的意义。
- AQS支持排他锁和共享锁机制
以上是java API中官方对AQS的定义,那么个人对AQS的理解和简单定义如下:
定义:AQS是一个支持自定义同步规则的框架,基于AQS,用户可以定义各种各样的同步锁。
AQS有什么作用?
从定义中,也可以直接推导出,AQS的作用如下:
- AQS最核心的用途就是作为一个同步器的基础。基于AQS可以实现自定义的同步器,比如ReentrantLock。
如何基于AQS实现自定义工具锁?
这个问题在学习AQS之前感觉不可思议,但是其实并不难,只要阅读AQS的JAVA api文档,就能快速实现一个简单的工具锁。
官方文档中说明,要实现一个自定义锁,关键步骤如下:
-
要基于AQS实现一把锁,首先需要定义一个内部类Sync,继承AQS
-
Sync需要实现以下方法,这也是使用AQS唯一的方式
- tryAcquire(int):尝试以排他的方式获取锁,是否能够获取要看int值的定义
- tryRelease(int):通过设置stat的值,来表达当前同步器已经不处于排他锁定状态。
- tryAcquireShared(int):尝试以共享方式获取锁,需要判断当前的状态是否支持其获取
- tryReleaseShared(int):尝试将stat设置为release状态
- isHeldExclusively():如果当前线程占有同步锁,则返回true
这些方法的默认实现,都是抛出一个异常UnsupportedOperationException。
官方提供的样例如下:
基于AQS实现一个不可重入锁,参考链接:https://docs.oracle.com/javase/8/docs/api/。
最核心的工作有2个,分别是:
- 设计stat代表的含义
- 这里0代表无锁状态,1代表被占用状态
- 然后封装一个内部类Sync,实现tryAcquire,tryRelease等方法
- 这是自定义锁的核心逻辑
AQS框架主流程分析
在深入学习源码之前,我们先梳理一下AQS框架的主流程,宏观地了解整个运行机制。
-
对一个自定义锁而言,我们首先会调用lock方法
-
然后lock方法会调用AQS的acquire方法,调用逻辑如下
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
-
在上面的逻辑中,tryAcquire是用户自定义的锁机制,也就是说,AQS框架调用子类Sync的tryAcquire方法,返回true或者false,true代表获得锁,false代表没获得。
- 如果获得锁,tryAcquire会修改stat,在逻辑上实现线程独占锁
-
如果线程没有获得锁,之后的逻辑就不需要用户关心了,直接进入AQS预先编织好的模板流程,线程进入等待队列,到队列中获得锁。
总的来说,我们可以发现,AQS把锁机制开放给用户,把复杂的同步逻辑留给了自己。如果是学过大数据的同学应该可以感觉到,这一点和MapReduce框架非常类似,用户只需要实现计算逻辑,大数据的调度等内容就全部交给框架了。其实这种设计背后是一个设计模式,我们通常称之为模板方法模式,或者回调函数模式。
lock方法源码分析
了解了AQS的核心流程之后,我们开始逐步分析源码。在本文中,我将尝试用泳道图来描述源码,这种方式感觉是目前为止,分析源码最好的方式,结构清晰,逻辑严谨,用起来特别爽。好,不废话了,源码过程如下:
提醒:如果纯粹是看上面的图,肯定会一脸懵逼,其实上面的图只是我个人用的一个记录,只要从lock方法一直跟进去,就能非常清晰的把这个图画出来。
对于上面的过程,再进行提炼,可以总结核心要点如下:
- 除了tryAcquire方法以外,其他的逻辑全部是AQS内置的
- 首先判断stat是否为0,如果是,则直接cas抢占锁,如果不是,则尝试判断这个锁是否是当前线程已经占用,如果是,则stat+x,也能成功获得锁。
- 如果以上2种情况获取锁失败,则会尝试进入队列,排队抢占锁。在入队的过程中,又有2个核心要点
- 首先,需要通过CAS的方式,将当前线程节点添加到双向链表的尾部
- 第二,当前线程在进入park状态前,通过CAS设置waitStatus的状态,确保前置节点在release时,会唤醒当前节点,否则会出现死锁的情况。
- 一旦以上2点得以保证,当前线程就可以安心地进入park,等待唤醒,从而获取锁了。如果当前线程是第二个节点,则将头节点的waitStatus置为SIGNAL。
- 此时头节点中并没有存放线程,因为第一个线程不进入队列,从而可以推测,当线程release时,无论是否在队列中,都会自动唤醒头结点中的next线程。
AQS的核心原理?
有了总体流程的把握,也有了过程代码的研究,下面提炼一下AQS的核心原理。
- AQS核心1:通过stat的来表达锁的占有与空闲,实现获取锁的失败与成功,这部分是实现者自定义的。
- AQS核心2:当线程通过自定义逻辑获取锁失败,则基于等待队列去获取锁,这部分是框架预制的功能。
AQS的核心原理就是一个atomic stat + 双向链表队列。用一个图来表示,则如下图
结论
- 通过全文的论述,在一定程度上回答了前言中的全部问题,当然,回答的深度、粒度、广度还都不够,只是入门性质的,后续还需优化和补充。
上一篇: 关于 MacBook Pro 的入门