简单说说JUC核心-AQS

AQS(AbstractQueuedSynchronizer)

一个同步工具类,使用了模版方法设计模式,内部维护了一个volatile修饰的state,维护了一个双向链表队列,是一个个Node连接起来的,每个Node内部存储了一个线程,使用AQS过程中大量通过CAS操作去修改state与对队列进行操作。

相关链接

AQS

核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。



Node-waitStatus

这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLEDSIGNALCONDITIONPROPAGATE0

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。



ReentrantLock

通过举例ReentrantLock的例子来说明AQS的重要性

ReentrantLock与AQS的关系

ReentrantLock内部维护了一个Sync抽象类继承于AQS,并根据不同的公平机制实现了两个子类FairSyncNonFairSync
AQS-sync


调用lock()方法

调用lock()方法

  1. sync.lock()根据是否是公平锁选择不同的子类的具体实现方法NonfairSync.lock()FairSync.lock()
  2. acquire(1),子类未重写此方法,调用AQS的方法
  3. tryAcquire(arg)根据子类的实现不同调用不同的方法,两个方法不同之处在于FairSync多调用了一个hasQueuedPredecessors()方法,用于查询队列中是否有等待的线程Node,若队列中没有等待的线程,则使用CAS操作去争夺锁;NonFairSync则是直接进行CAS操作去争夺锁
  4. 若调用tryAcquire(arg)返回false,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)将线程放进队列中等待;其中addWaiter()方法操作是将新的nodeCAS操作将node加到队列尾部(其中在JDK1.9之后,引入VarHandle来操作将当前Node加在oldTail之后),其中enq()方法是CAS操作失败后,重复使用CAS操作直至成功加入队尾;acquireQueued()方法,是通过再次确认当前线程是否能够获取锁,若不能则执行LockSupport.park()进行当前线程的暂停

varHandle

在JDK1.9之后才出现,可以对普通属性进行原子性操作,且比反射快,可以理解为直接操作二进制码。

调用unlock()方法

  1. unlock()方法调用sync.release(1)方法
  2. sync.release(1)调用tryRelease(arg)去释放锁,由于锁是可重入的,所以tryRelease()执行结束后不一定释放,所以tryRelease()方法执行完,会更新锁的状态state,若state0,则会释放当前占有线程
  3. 释放成功后,判断等待队列中的head结点,即第一个等待线程,判断其状态waitStatus,若不为0,则执行unparkSuccessor()方法,进行下一个线程的唤醒操作