文章目录
本系列文章:
多线程(一)线程与进程、Thread
多线程(二)Java内存模型、同步关键字
多线程(三)线程池
多线程(四)显式锁、队列同步器
多线程(五)可重入锁、读写锁
多线程(六)线程间通信机制
多线程(七)原子操作、阻塞队列
多线程(八)并发容器
多线程(九)并发工具类
多线程(十)多线程编程示例
一、Lock
1.1 初识Lock
在Lock接口出现之前,主要是靠synchronized关键字实现锁功能。在JDK1.5之后,出现了Lock接口,Lock也能实现锁功能。相比synchronized,Lock的灵活性更好。
Lock接口中声明的API:
//获取锁
void lock();
//获取锁,在此过程中能够响应中断
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞的获取锁,调用该方法立即返回,获取锁返回true,反之返回fasle
boolean tryLock();
//超时获取锁,在超时内或者未中断的情况下能够获取锁
//当前线程在以下3中情况下回返回:
//1、当前线程在超时时间内获取了锁
//2、当前线程在超时时间内被中断
//3、超时时间结束,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取等待通知组件,该组件和当前锁绑定,当前线程只有获取了锁,才能调用该组件
//的wait()方法。调用后,当前线程将释放锁
Condition newCondition();
// 释放锁
unlock();
在Lock接口的实现类中,ReentrantLock是最常见的,其常用方式:
//创建一个Lock接口实例
Lock lock = new ReentrantLock();
//申请锁
lock.lock();
try {
.......
} finally {
//在finally块中释放锁,以避免锁泄漏
lock.unlock();
}
Lock API使用总结:
创建Lock接口的实例
。如没有特别的要求,可以使用实现类ReentrantLock的实例作为显式锁使用,这是一个可重入锁;在访问共享数据前申请相应的显式锁
。在临界区中访问共享数据,一般将上面的try代码作为临界区
。共享数据访问结束后释放锁,为了避免锁泄漏,在finally块中释放锁
。
1.2 synchronized和Lock的区别*
因为Lock的最常见实现是ReentrantLock,所以下面会穿插着ReentrantLock来对比。
synchronized和ReentrantLock相同点:都是用来协调多线程,对共享对象、变量的访问都是可重入锁,同一线程可以多次获得同一个锁,都保证了可见性和互斥性。
具体的不同点:
- 1、底层实现
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
ReentrantLock是一个具体类,是API层面的锁。
synchronized隐式获得释放锁,ReentrantLock显式的获得、释放锁
。 - 2、是否自动释放锁
synchronized不需要用户手动释放锁
,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用。线程执行发生异常,JVM会让线程释放锁。
ReentrantLock需要在finally块中手动释放锁
,若没有手动释放可能导致死锁现象。 - 3、是否能判断是否获得了锁
synchronized不能判断。
Lock可以判断。 - 4、加锁是否公平
synchronized非公平锁
。
ReentrantLock两者都可以,默认是非公平锁
。 - 5、是否可以有多个唤醒条件
synchronized不能。
ReentrantLock可用来分组唤醒需要唤醒的线程
。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。 - 6、是否可中断
synchronized不可中断
,除非抛出异常或者正常运行完成。
ReentrantLock可中断
。 - 7、是否可设置超时
synchronized 获取锁无法设置超时;
ReentrantLock 可以设置获取锁的超时时间。 - 8、悲观与乐观策略
synchronized是同步阻塞,使用的是悲观并发
策略;
Lock是同步非阻塞,采用的是乐观并发
策略。
整体上来说Lock是synchronized的扩展版,Lock提供了可轮询的(tryLock 方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。
同时,Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁。在大部分情况下,非公平锁是高效的选择。
二、 AQS
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
AQS使用一个volatile的int类型的成员变量state来表示同步状态
,通过CAS修改同步状态的值。当线程调用lock方法时 ,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1。如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待
。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享 )构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。
如果说JUC的基础是CAS的话,那么AQS就是整个Java并发包的核心,ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等等都用到了它
。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
- ReentrantLock
Lock的常用实现类是ReentrantLock。在ReentrantLock源码中,很多方法都是调用sync
变量来实现的:
private final Sync sync;
Sync是ReentrantLock的静态内部类,继承了AQS:
abstract static class Sync extends AbstractQueuedSynchronizer
2.1 AQS的整体设计
2.1.1 模板方法模式
AQS的设计是使用模板方法模式
。AQS是一个抽象类,它将一些方法开放给子类进行重写,而AQS中的模板方法又会调用被子类所重写的方法。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
//该线程是否正在独占资源。只有用到condition才需要去实现它。
isHeldExclusively()
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
tryReleaseShared(int)
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们只需实现tryAcquire/tryRelease 、 tryAcquireShared/tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock 。
AQS中的tryAcquire()方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock中NonfairSync(继承AQS)会重写该方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
AQS中的模板方法acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
继承AQS的NonfairSync调用AQS中模板方法acquire时,就会调用已经被自身重写的tryAcquire方法。
2.1.2 同步队列中的Node
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程。当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
在AQS有一个静态内部类Node
,Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。Node的一些属性:
//指示节点正在共享模式下等待的标记
static final Node SHARED = new Node();
//指示节点正在独占模式下等待的标记
static final Node EXCLUSIVE = null;
//节点状态
volatile int waitStatus;
//当前节点/线程的前驱节点
volatile Node prev;
//当前节点/线程的后继节点
volatile Node next;
//加入同步队列的线程引用
volatile Thread thread;
//等待队列中的下一个节点
Node nextWaiter;
waitStatus的取值有以下几种:
//表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),
//会触发变更为此状态,进入该状态后的结点将不会再变化
static final int CANCELLED = 1;
//表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
static final int SIGNAL = -1;
//表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,
//CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
static final int CONDITION = -2;
//共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点
static final int PROPAGATE = -3;
//新结点入队时的默认状态
static final int INITIAL = 0;
AQS中有两个成员变量:
private transient volatile Node head;
private transient volatile Node tail;
从这两个变量可以看出:每个节点拥有其前驱和后继节点,因此这是一个双向队列
。
节点如何进行入队和出队这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作
。
AQS实际上通过头尾指针来管理同步队列
,图示:
同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
同步器将节点加入到同步队列的过程:
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程:
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
2.1.3 AQS提供的模板方法*
在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理
。
AQS提供的模板方法可以分为3类:
1、独占式获取与释放同步状态;
2、共享式获取与释放同步状态;
3、查询同步队列中等待线程情况。
- 1、独占式锁模板方法
//独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返
//回,否则,将进入同步队列等待,该方法将会调用AQS子类重写的
//tryAcquire(int arg)方法
public final void acquire(int arg)
//与acquire(int arg)相似,但是该方法能响应中断,当前线程未获取到
//同步状态而进入同步队列中。如果当前线程被中断,则该方法会抛出
//InterruptedException
public final void acquireInterruptibly(int arg) throws InterruptedException
//在acquireInterruptibly(int arg) 基础上增加了超时限制,如果当前
//线程在超时时间内没有获取到同步状态,那么将返回false,获取到了
//返回true
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
//独占式地释放同步状态,该方法会在释放同步状态之后,将同步队列中
//的第一个节点包含的线程唤醒
public final boolean release(int arg)
- 2、共享式锁模板方法
//共享式地获取同步状态,如果当前线程未获取到同步状态,将会进入
//同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线
//程获取到同步状态
public final void acquireShared(int arg)
//在acquireShared方法基础上增加了能响应中断的功能
public final void acquireSharedInterruptibly(int arg) throws InterruptedException
//在acquireSharedInterruptibly基础上增加了超时等待的功能
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
//共享式释放同步状态
public final boolean releaseShared(int arg)
- 3、查询同步队列中等待线程情况
//获取等待在同步队列上的线程集合
public final Collection< Thread > getQueuedThreads()
2.1.4 AQS和同步组件的关系
AQS的子类,常常被定义为自定义同步组件的静态内部类(如ReentrantLock中的Sync,及Sync的子类NonfairSync和FairSync)。
同步器(AQS)自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件
。
AQS和同步组件的关系可以归纳总结为这么几点:
- 1、同步组件的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
- 2、AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
- 3、AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
- 4、在重写AQS的方法时,使用AQS提供的getState()、setState()、compareAndSetState()方法进行修改同步状态。
2.2 独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
2.2.1 独占锁的获取
独占锁获取的典型场景:调用ReentrantLock的lock()方法,实际上会调用AQS的acquire()方法。这个方法的含义是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。AQS中的acquire()方法源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()方法流程:
- tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而队列中可能还有别的线程在等待);
- 如果获取资源失败,addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
图示:
acquire()方法中涉及到4个方法:
- 1、tryAcquire(int)
尝试去获取独占资源
。如果获取成功,则直接返回true,否则直接返回false。 - 2、addWaiter(Node)
当线程获取独占式锁失败后,将当前线程加入到等待队列的队尾,并返回当前线程所在的结点
。源码:
private Node addWaiter(Node mode) {
//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode);
//当前尾节点是否为null
Node pred = tail;
if (pred != null) {
//将当前节点尾插入的方式插入同步队列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程
enq(node);
return node;
}
addWaiter方法的逻辑主要分为两个部分:
- 当前同步队列的尾节点为null,调用方法enq()插入;
- 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail方法)的方式入队。
同时还会有一个逻辑:如果 if (compareAndSetTail(pred, node))
为false,会继续执行到enq()方法,compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋进行重试。因此,enq()方法可能承担两个任务:
- 处理当前同步队列尾节点为null时进行入队操作;
- 如果CAS尾插入节点失败后负责自旋进行尝试。
enq(final Node node)
用于将node加入队尾,源码:
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) {
// 1.队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 2.尾插入,CAS操作失败自旋尝试
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第1步中会先创建头结点,说明同步队列是带头结点的链式存储结构。
带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。
带头节点的队列初始化时机是在tail为null时,即当前线程是第一次插入同步队列。compareAndSetTail(t, node)
方法会利用CAS操作设置尾节点,如果CAS操作失败会在for (;;)for死循环中不断尝试,直至成功return返回为止。因此,enq()方法的逻辑:
- 在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化;
- 自旋不断尝试CAS尾插入节点直至成功为止。
- 3、acquireQueued(final Node node, int arg)
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了,j接下来就要考虑怎么获取资源了,也就是acquireQueued的逻辑:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 获得当前节点的先驱节点
final Node p = node.predecessor();
// 2. 当前节点能否获取独占式锁
// 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
if (p == head && tryAcquire(arg)) {
//队列头指针用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
整体来看这是一个这又是一个自旋的过程(for循环),代码首先获取当前节点的先驱节点,如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁。反之,获取锁失败进入等待状态。
在acquireQueued(final Node node, int arg)
方法里,有一段代码是表示:获取锁的节点出队:
//队列头结点引用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
从acquireQueued方法中得知:当获取锁失败的时候会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法。shouldParkAfterFailedAcquire()方法源码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到前驱的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被GC回收
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire方法主要用于检查状态,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
如果线程找好安全休息点后,那就可以安心去休息了。parkAndCheckInterrupt方法就是让线程去休息,真正进入等待状态:
private final boolean parkAndCheckInterrupt() {
//调用park()使线程进入waiting状态
LockSupport.park(this);
//如果被唤醒,查看自己是不是被中断的
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
到这里,就可以总结:acquireQueued()在自旋过程中主要完成了两件事情:
- 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出;
- 获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞。
公平锁和非公平锁的不同
- 非公平锁在调用lock方法后,首先就会调用CAS进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在CAS失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接CAS抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次CAS都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
2.2.2 独占锁的释放
独占锁释放的典型场景:ReentrantLock中的unlock方法。该方法最终会调用到AQS中的release方法,release会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。源码:
public final boolean release(int arg) {
//同步状态释放成功
if (tryRelease(arg)) {
Node h = head;//找到头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒等待队列里的下一个线程
return true;
}
return false;
}
跟tryAcquire()一样,tryRelease方法是需要独占模式的自定义同步器去实现的。
unparkSuccessor(Node node)
方法用于唤醒等待队列中下一个线程,源码:
private void unparkSuccessor(Node node) {
//node一般为当前线程所在的结点
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
源码的关键信息请看注释,首先获取头节点的后继节点,当后继节点存在的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步证明获得锁的过程是一个FIFO(先进先出)的过程。
2.2.3 独占锁的获取和释放总结
- 1、线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
- 2、线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
- 3、释放锁的时候会唤醒后继节点。
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点
。
2.2.4 可中断式获取锁
lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性
,可响应中断式锁可调用方法lock.lockInterruptibly()来响应中断,该方法其底层会调用AQS的acquireInterruptibly方法,源码:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
//线程获取锁失败
doAcquireInterruptibly(arg);
}
在获取同步状态失败后就会调用doAcquireInterruptibly方法:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//将节点插入到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//获取锁出队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//线程中断抛异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时,即线程阻塞时该线程被中断,代码抛出被中断异常。
2.2.5 超时等待式获取锁
通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:
- 在超时时间内,当前线程成功获取了锁;
- 当前线程在超时时间内被中断;
- 超时时间结束,仍未获得锁返回false。
该方法会调用AQS的方法tryAcquireNanos(),源码:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
//实现超时等待的效果
doAcquireNanos(arg, nanosTimeout);
}
这段源码最终是靠doAcquireNanos方法实现超时等待的效果,该方法源码:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//1. 根据超时时间和当前时间计算出截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//2. 当前线程获得锁出队列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新计算超时时间
nanosTimeout = deadline - System.nanoTime();
// 3.2 已经超时返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 线程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 线程被中断抛出被中断异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquireNanos方法逻辑:
该方法逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上,在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是8:10,超时时间是10分钟,那么根据deadline = System.nanoTime() + nanosTimeout
计算出刚好达到超时时间时的系统时间就是8:10+10 = 8:20。
然后根据deadline - System.nanoTime()
就可以判断是否已经超时了,比如,当前系统时间是8:30,很明显已经超过了理论上的系统时间8:20,deadline - System.nanoTime()
计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过LockSupport.parkNanos使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。
2.3 共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
2.3.1 共享锁的获取
AQS中共享锁的获取方法为acquireShared:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁。否则,表明获取锁失败,会执行doAcquireShared方法,进入等待队列,直到获取到资源为止才返回。
- doAcquireShared(int arg)
doAcquireShared方法的源码为:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//加入队列尾部
boolean failed = true;//是否成功标志
try {
boolean interrupted = false;//等待过程中是否被中断过的标志
for (;;) {
final Node p = node.predecessor();//前驱
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态。不过多了个拿到资源后,还会去唤醒后继线程的操作。
acquireShared()的流程:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
2.3.2 共享锁的释放
共享锁的释放在AQS中的方法releaseShared:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。一句话总计的话:释放掉资源后,唤醒后继
。
跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。
例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
- doReleaseShared()
当成功释放同步状态之后即tryReleaseShared会继续执行doReleaseShared方法:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)// head发生变化
break;
}
}
此方法主要用于唤醒后继。