
JAVA之AQS底层原理
LockSupport
LockSupport是线程等待唤醒机制(wait/notify)的改良版本。LockSupport中的 park() 和 unpark() 的作用分别是阻塞线程和接触阻塞线程。
3种让线程等待和唤醒的方法(线程通信)
方式1:使用Object中的wait()方法让线程等待,notify()方法唤醒线程
synchronized + wait + notify
方式1:使用Object中的wait()方法让线程等待,notify()方法唤醒线程
wait、notify的限制:
- 我们发现 wait 和 notify 如果不在一个代码块里面,必须与 synchronized 搭配使用,否则会报错。
- 如果我们先使用notify、再使用wait,因为wait是后执行了,所以不能被唤醒。
方式2:使用JUC包中Condition的await()方法让线程等待,signal()方法唤醒线程
Lock + await + signal
await、signal的限制:
- 和 wait 、notify 的问题一模一样,他们的底层机制是一样的。
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
park + unpark,每个线程都有一个 “许可证” ,只有 0 和 1,默认为 0。unpark(Thread t) 方法发放许可证,没许可证就不允许放行。
LockSupport的优势:
- 既不用synchronized或Lock。
- 先唤醒,再阻塞,也能够被唤醒。因为线程已经有了“许可证”了,所以park()方法相当于没执行。
park底层调用了unsafe类的park本地方法。
调用一次 unpark 就加 1,变为 1。调用一次 park 会消费许可证,变回 0。重复调用 unpark 不会积累凭证。
AQS理论
- AQS(AbstractQueuedSynchronizer),抽象的队列同步器。ReentrantLock类里,有一个内部类Sync就是继承的AQS类。
- AQS是用来构建锁 或者 其他同步器组件的重量级基础框架及整个JUC体系的基石。通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
AQS的作用
和AQS有关的 :ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore......
锁和同步器的关系:
- 锁,面向锁的使用者。
- 同步器,面向锁的实现者。
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁的分配。这个机制主要用的是 CLH队列 的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过 CAS 自旋以及 LockSupport.park() 的方式,维护state变量的状态,使并发达到同步的控制效果。
AQS源码体系
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队,将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对State值的修改。
- 表示同步状态的int成员变量
state为 0 就是没人占用,可以去获取资源。大于等于 1,有人占用资源,需要排队。
- CLH队列
CLH队列,是一个双向队列。通过自旋等待,state变量判断是否阻塞。
内部类Node作为载体,装的是需要排队的线程。
- 内部类Node
队列中每一个排队的个体就是一个Node。Node有前结点prev 、 后结点next 、头指针head 、尾指针tail,用于完成双向队列。
Node类里有两个模式:SHARED(表示线程以共享的模式等待锁)、EXCLUSIVE(表示线程以独占的方式等待锁)
类中也有一个int类型的状态变量 waitStatus。意思是等候区其他线程的等待状态。
从ReentrantLock开始解读AQS
ReentrantLock类中有个子类Sync继承了AQS类,NonfairSync类和FairSync继承了Sync类。
公平锁和非公平锁实现方法的唯一区别就在于:公平锁在获取同步状态时多了一个限制条件:hasQueuePredecessors()。
这个是公平锁加锁时判断等待队列中是否存在有效节点的方法。因为公平锁是先排先得。
new一个ReentrantLock类时,不传参数默认是非公平锁(NonfairSync),传入true是公平锁(FairSync)
- 公平锁:讲究先来先到,线程在获取锁时,如果等待队列中以及有线程在等待,那么当前线程就会进入等待队列中。
- 非公平锁:不管是否有等待队列,都会去尝试获得锁。
AQS非公平锁就是,一来就先去插队,如果插队失败,才去乖乖的排队。
银行办理业务案例
整个ReentrantLock的加锁过程,可以分为三个阶段:
- 尝试加锁
- 加锁失败,线程进入AQS队列
- 线程进入队列后,进入阻塞状态
lock()上锁
调用的是sync类的lock()方法。
如果是FairSync:
如果是NonfairSync:
当第一个顾客发现没人窗口没人后,开始办理业务。state变为1,占用顾客的线程是 currentThread。
第一个客户已经占用了窗口,没那么快完成。第二个客户也调用lock()方法,发现窗口被占用,只能去排队:
AQS的tryAcquire方法
我们进入tryAcquire方法发现没有逻辑代码,直接抛出异常。这就是典型的模板方法设计模式。意思是所有子类必须实现这个方法,不实现父类就抛出异常。
然后发现 ReentrantLock 的 内部类NofairSync 重写了这个方法:
这个方法其实调用的是内部类Sync 的 nonfairTryAcquire 方法。
我们传入的第二位顾客再次发现,有人在办理业务,返回 false。
在acquire方法中 !tryAcquire(arg) 取反为 true ,继续判断下面的方法。
- AQS的addWaiter方法
acquire传入的是 Node.EXCLUSIVE 参数(结点的模式);结点进入队列。
enq 方法(将节点插入队列):
- AQS的acquireQueued方法
传入的参数是 (addWaiter(Node.EXCLUSIVE), arg)。
shouldParkAfterFailedAcquire 方法:
parkAndCheckInterrupt 方法:
此时这个acquireQueued方法还没有结束,会被卡在parkAndCheckInterrupt方法内部,如果这个线程被unpark了。就会继续执行acquireQueued方法的代码。
unlock释放锁
调用的是sync类的lock()方法。
调用AQS的release方法,arg=1.
tryRelease 方法,也是一个模板方法,ReentrantLock类的Sync重写了这个方法。
unparkSuccessor方法,释放锁!
此时线程被唤醒,前面acquireQueued里阻塞的其他线程就继续往下执行。
