目录
0.locks包简介
locks包下类如下(jdk1.8)
1.AQS简介
1.1 AQS是什么
- AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
- JUC当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列,条件队列,独占获取,共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer,简称AQS
- AQS是一个用来构建锁和同步器的抽象同步框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是 基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
- 优势:
- 基于AQS来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
- 在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量
1.2 AQS框架实现的一些点
- AQS内部维护属性volatile int state(32位)
- state表示资源的可用状态
- state的三种访问方式
- getState(),setState(),compareAndSetState()
- AQS定义两种资源共享方式
- Exclusive:独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
- AQS定义两种队列
- 同步等待队列
- 条件等待队列
1.3 AQS的层次结构
可以发现AQS是继承自AbstractOwnableSynchronizer这个类,我们来看看这个类的完整代码如下:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
/** Use serial ID even though all fields transient. */
private static final long serialVersionUID = 3737899427754241961L;
/**
* 默认构造函数
*/
protected AbstractOwnableSynchronizer() { }
/**
* 独占模式下拥有锁的线程
*/
private transient Thread exclusiveOwnerThread;
/**
* 设置拥有锁的线程
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
/**
* 获取拥有锁的线程
*/
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
- 可以发现该类只定义了独占锁的拥有线程,并提供了它的设置和获取方法
2.AQS原理及源码详解
2.1 状态
//锁状态:1表示有线程占用锁 0表示没有任何线程占用锁
private volatile int state;
/**
* 获取state的值
*/
protected final int getState() {
return state;
}
/**
* 设置state的值
*/
protected final void setState(int newState) {
state = newState;
}
/**
* 通过CAS原子地修改state的值
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
我们都知道synchronized底层是使用的内核态的mutex锁,这样加锁和释放锁时就要在内核态和用户态切换,这样就使得synchronized效率比较低,而state是用来用户态实现独占锁的
- 在state为0的时候,表示锁未被任何线程占用
- 在state为1的时候,表示锁被某一个线程锁占用
- 在state>1的时候,表示占用锁的线程多次加锁的这个次数即重入次数
以下通过图解来理解使用state实现独占锁和重入锁的过程:
(1)state实现独占锁的图解
- 1.如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己
- 2.如果线程1加锁了之后,线程2跑过来加锁,state的值不是0,所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有线程已经加锁了,接着线程2会看一下,是不是自己之前加的锁,当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
- 3.接着,线程2会将自己放入AQS中的同步等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后唤醒它,自己就可以重新尝试加锁了
- 4.接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁,他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null
-
5.接下来,会从同步队列的队头唤醒线程2重新尝试加锁。线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从同步队列中出队了
(2)state实现可重入锁的图解
那么如果是线程1加锁以后,又再次去加锁会怎么样呢?以下是重入锁的实现逻辑:
所以state>1表示的是重入锁的数量,这样在释放的时候,如下:
- 1.在第一次调用unlock释放锁的时候,将state先减1,然后判断state是否变为0,可以看到暂时还为1并没有变为0,就什么都不做,代表线程1只是释放了内部的一层重入锁
- 2.在第二次调用unlock释放锁的时候,将state先减1,然后判断state是否变为0,变为0,就将加锁线程设置为null,代表线程1彻底释放锁
2.2 节点Node
static final class Node {
//共享模式的标记
static final Node SHARED = new Node();
//独占模式的标记
static final Node EXCLUSIVE = null;
//表示线程已被取消(等待超时或者被中断)
static final int CANCELLED = 1;
//表示后继线程需要被唤醒(unpaking)
static final int SIGNAL = -1;
//表示当前节点不在同步队列中,它在条件队列中,结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中
static final int CONDITION = -2;
//表示下一次共享式同步状态会被无条件地传播下去
static final int PROPAGATE = -3;
//等待状态,初始为0
volatile int waitStatus;
//前置节点
volatile Node prev;
//后置节点
volatile Node next;
//记录当前节点关联的线程(节点值)
volatile Thread thread;
/**
* 对于同步阻塞队列
* 为null表示EXCLUSIVE,即在独占模式下
* 为SHARED表示在共享模式下
*
* 对于条件队列(单项链表)
* 表示下一个在condition上等待的线程节点
*/
Node nextWaiter;
//判断此节点是否处于共享模式下
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//此构造函数被用在建立头结点或者共享标志的时候使用
Node() { // Used to establish initial head or SHARED marker
}
//此构造函数在addWaiter方法中被使用
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//此构造函数被在ConditionObject内部类中使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node其实既是AQS的同步阻塞队列中的节点,也是条件队列中的节点
- 作为同步阻塞队列中的节点是一个双链表节点:
- 作为条件队列中的节点:(以下仅仅是表示出了节点,上面的其他属性还在节点中)
对nextWaiter的特别说明:
其实nextWaiter在同步阻塞队列中和条件队列中充当了不同的角色
- 在同步阻塞队列中
- nextWaiter表示该节点是独占模式还是共享模式
- 在条件队列中
- nextWaiter表示指向下一个等待者的指针
节点中除过包含指向前后节点的指针(prev和next),还包括需要同步的线程本身(thread)和其等待状态(waitStatus)
- 变量waitStatus则表示当前Node结点的等待状态,共有以下5种取值
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
-
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
2.3 同步队列
AQS核心思想是:
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态(state=1)。
- 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到同步队列中。
同步队列是一个带有头结点(头结点的值thread=null)的双向链表
2.4 加锁函数acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
函数流程:
- 1.tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加锁一次,而CLH队列中可能还有别的线程在等待);
- 2.如果tryAcquire加锁不成功,addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- 3.acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
(1)tryAcquire(int)
- 此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。
- AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现
- 这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
(2)addWaiter(Node)
/**
* 此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。
* @param mode 共享或者独占模式
* @return
*/
private Node addWaiter(Node mode) {
//创建一个当前线程的Node节点,并设置它的模式为传入的参数mode
Node node = new Node(Thread.currentThread(), mode);
//取得尾节点
Node pred = tail;
//尾结点不为空,即同步阻塞队列不为空,就将上述新建的节点node插入到队列的尾部
if (pred != null) {
node.prev = pred;
//原子地设置尾节点为node
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//初始化队列,并将节点加入到队列尾部
enq(node);
return node;
}
可以看到addWaiter又使用了enq(Node)
/**
* 此方法在是在同步阻塞队列没有被初始化的时候(tail=null),先初始化队列
* 如果已经被初始化过,就将节点加入到队列尾部
*
* 可以看到队列被初始化是创建一个新的空节点作为头结点,即同步阻塞队列是带头结点的双向链表,在这里就可以看到
* @param node
* @return
*/
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
//队列没有被初始化,初始化队列 head=tail=new Node()
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
//队列已经被初始化,将节点加入到队列尾部
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* 通过Unsafe的CAS来修改头结点
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
/**
* 通过Unsafe的CAS来修改尾结点
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
同步阻塞队列初始化:
- 可以看到同步阻塞队列在初始化的时候,是将head和tail都指向一个空节点(即不存储任何线程的节点),所以同步阻塞队列是一个带头结点的双向链表
for(;;)说明:
- 这里其实是使用了一种经典用法——volatile+CAS+自旋
- head和tail都是volatile变量,保证了可见性和有序性,然后通过CAS保证了原子性,通过自旋来阻塞到直到修改成功为止
- 这种用法也可在Unsafe类的getAndAddInt方法或者AtomicInteger类的getAndUpdate方法中找到
(3)acquireQueued(Node, int)
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了
- acquireQueued内部也是一个死循环,只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;
- 若tryAcquire成功,表示获取同步状态成功,将此结点设置为头结点;
- 若是非老二结点,或者tryAcquire失败,则进入shouldParkAfterFailedAcquire去判断判断当前线程是否应该阻塞,
- 若可以,调用parkAndCheckInterrupt阻塞当前线程,直到被中断或者被前驱结点唤醒。
- 若还不能休息,继续循环。
2.5 释放锁的函数
3.使用AQS实现自己的锁的案例
- 一般通过定义内部类Sync继承AQS
- 将同步器所有调用都映射到Sync对应的方法