提起AQS(AbstractQueuedSynchronizer),不得不让我们想起多线程的同步的几种的实现方式,主要是wait/notify,synchronized,ReentrantLock,下面我们会介绍几种同步的实现方式,从而推理出AQS的加锁的过程。
import java.util.concurrent.atomic.AtomicInteger;
public class SpinLocksDemo {
//标识--是否有线程在同步块---是否有线程上锁成功
private volatile AtomicInteger state = new AtomicInteger(0);
//利用自旋加锁
public void lock() {
while (!state.compareAndSet(0, 1)) {
}
}
public void unlock() {
state.compareAndSet(1, 0);
}
}
我们测试一下,书写一个测试类如下:
import java.util.concurrent.CountDownLatch;
public class Test {
static CountDownLatch countDownLatch = new CountDownLatch(1000);
public static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
SpinLocksDemo lock = new SpinLocksDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
lock.lock();
try {
a++;
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + ":" + a);
} finally {
lock.unlock();
}
}).start();
}
countDownLatch.await();
System.out.println("a的值是:" + a);
}
}
运行结果:最后输出 a的值是: 1000
我们可以看到最终的结果是对的,可以验证我们的锁是对的,但是缺点也很明显:耗费CPU资源。没有竞争到锁的线程会一直占用CPU资源进行CAS操作,假如一个线程得到锁后完成业务花费N秒,与此同时,其他线程就会处于空转、浪费cpu资源的状态。
所以我们可不可以让CAS不成功的线程让出CPU。于是有了下面的方式
import java.util.concurrent.atomic.AtomicInteger;
public class SpinLocksAndYieldDemo {
//标识--是否有线程在同步块---是否有线程上锁成功
private volatile AtomicInteger state = new AtomicInteger(0);
//利用自旋加锁
public void lock() {
while (!state.compareAndSet(0, 1)) {
//让出CPU
Thread.yield();
}
}
public void unlock() {
state.compareAndSet(1, 0);
}
}
我们再书写一个测试类如下:
import java.util.concurrent.CountDownLatch;
public class Test {
static CountDownLatch countDownLatch = new CountDownLatch(1000);
public static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
//SpinLocksDemo lock = new SpinLocksDemo();
SpinLocksAndYieldDemo lock = new SpinLocksAndYieldDemo();
//SpinLocksAndSleepDemo lock = new SpinLocksAndSleepDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
lock.lock();
try {
a++;
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + ":" + a);
} finally {
lock.unlock();
}
}).start();
}
countDownLatch.await();
System.out.println("a的值是:" + a);
}
}
运行的结果如下:a的值为: 1000
可以看到我们的CPU的占有率没有达到100%。但是我们要解决自旋锁的性能问题需要让竞争锁的失败的线程不空转,在获取不到锁的时候能把CPU资源给让出来,yield方法就能让出CPU资源,当线程竞争失败时,会调用yield方法让出CPU。但是yield方法,只会短暂的让出CPU,下次执行的时候,可能CPU还会执行刚才那个加锁失败的线程,自旋+yield的方式并没有完全的解决问题,当系统只有两个线程竞争锁时,yield是有效的。但是如果有2000个线程,这个时候竞争也会上去,空转也会上去,没有从根本上解决问题。
于是又有了一种新的方式,如下:
public class SpinLocksAndSleepDemo{
//标识--是否有线程在同步块---是否有线程上锁成功
private volatile AtomicInteger state = new AtomicInteger(0);
//利用自旋加锁
public void lock() {
while (!state.compareAndSet(0, 1)) {
//让当前线程睡一会儿
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void unlock() {
state.compareAndSet(1, 0);
}
}
书写测试代码如下:
import java.util.concurrent.CountDownLatch;
public class Test {
static CountDownLatch countDownLatch = new CountDownLatch(1000);
public static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
SpinLocksAndSleepDemo lock = new SpinLocksAndSleepDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
lock.lock();
try {
a++;
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + ":" + a);
} finally {
lock.unlock();
}
}).start();
}
countDownLatch.await();
System.out.println("a的值是:" + a);
}
}
运行结果如下:a的值为: 1000
可以看到结果是正确的,但是sleep的时间为什么是50?这个时间我们从那获取到呢?可以说这个问题是无解,因为你永远不知道这个线程执行的时间,所以这种方案也是不可行的。于是有了第四种类似AQS那种,具体如下:
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
public class SpinLocksAndParkDemo {
//标识--是否有线程在同步块---是否有线程上锁成功
private volatile AtomicInteger state = new AtomicInteger(0);
//等待的队列
private volatile Queue<Thread> parkQueue = new LinkedBlockingQueue<>();
//利用自旋加锁
public void lock() {
while (!state.compareAndSet(0, 1)) {
park();
}
}
private void park() {
Thread thread = Thread.currentThread();
//将当前线程加入到等待队列中
parkQueue.add(thread);
//将当前的线程释放CPU阻塞
LockSupport.park();
}
public void unlock() {
lock_notify();
}
private void lock_notify() {
state.compareAndSet(1, 0);
//得到要唤醒的线程的头部线程
Thread thread = parkQueue.poll();
//唤醒等待线程
LockSupport.unpark(thread);
}
}
书写测试类如下:
import java.util.concurrent.CountDownLatch;
public class Test {
static CountDownLatch countDownLatch = new CountDownLatch(1000);
public static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
//SpinLocksDemo lock = new SpinLocksDemo();
//SpinLocksAndYieldDemo lock = new SpinLocksAndYieldDemo();
//SpinLocksAndSleepDemo lock = new SpinLocksAndSleepDemo();
SpinLocksAndParkDemo lock = new SpinLocksAndParkDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
lock.lock();
try {
a++;
System.out.println(Thread.currentThread().getName() + ":" + a);
countDownLatch.countDown();
} finally {
lock.unlock();
}
}).start();
}
countDownLatch.await();
System.out.println("a的值是:" + a);
}
}
运行的结果如下:a的值为: 1000
通过上面的几种的实现方式,我们得出了AQS的实现的大概方式,但是现实的AQS比我们要想的复杂的多,下面就让我们通过AQS的源码来一一解析吧!
首先我们要看看AQS的结构的类图
我们先打开AbstractQueuedSynchronizer的源码,通过上面的结构图,我们可以知道该类维护了一个Node内部类,于是我们查看Node的源码如下,主要是用来实现上面的我们提到的队列。
static final class Node {
//指示节点正在共享模式下等待的标记
static final Node SHARED = new Node();
//指示节点正在以独占模式等待的标记
static final Node EXCLUSIVE = null;
//waitStatus值,指示线程已取消 cancel
// 这个节点已经被取消 canceled 这样可读性强
static final int CANCELLED = 1;
//waitStatus值,指示后续线程需要释放 signal
// 这个节点的后继被阻塞,因此当前节点在取消必须释放它的后继
static final int SIGNAL = -1;
//waitStatus值,指示线程正在等待条件 condition
// 这个节点在条件队列里面
static final int CONDITION = -2;
//waitStatus值,表示下一个被默认的应该无条件传播的等待状态值 propagate
static final int PROPAGATE = -3;
/*
* SIGNAL:这个节点的后继被(或即将)阻塞(通过park),因此当前节点在释放或取消时必须释放它的后继。为了避免竞争,acquire方法必须首先表明它们需要一个信号,然后重试原子获取,当失败时,阻塞。
*
* CANCELLED:由于超时或中断,该节点被取消。节点不会离开这个状态。特别是,取消节点的线程不会再次阻塞。
*
* CONDITION:此节点当前处于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。
*
* PROPAGATE:释放的共享应该传播到其他节点。在doReleaseShared中设置这个(仅针对头节点),以确保传播继续,即使其他操作已经干预。
*
* 0:以上都不是
*/
volatile int waitStatus; // 默认值0,什么都不是
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//节点中的值
volatile Thread thread;
//下一个等待节点
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;
}
//用于建立初始标头或SHARED标记
Node() {
}
//addWaiter时候调用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//Condition时候调用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
所以可以根据上面的代码,大概的想想一下,这个队列是什么样,具体如下图:
金手指1:AQS内部类Node
AQS本质是一个非循环的双向链表(也可以称为队列),所以它是由一个个节点构成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node为基本元素操作的,那么在这个Node类中需要保存什么信息呢?
从上面的图看,至少四个 prev next 两个Node类型变量做指针,thread 存放节点的值,因为AQS队列的节点就是存放线程的,所以这个值类型就是Thread,nextWaiter也是Node类型,表示下一个等待节点, waitStatus表示当前节点等待状态
volatile int waitStatus; //当前节点等待状态
volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //节点中的值
Node nextWaiter; //下一个等待节点
//指示节点共享还是独占,默认初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
记住一个Node节点的六个属性(共享/独占算一个),下面看源码就轻松些
处在同步队列中使用到的属性(本文用:加锁、解锁)包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
处在等待队列中使用到的属性(下一篇博文用:阻塞、唤醒)包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
AQS队列是工作队列、同步队列,是非循环双向队列,当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
等待队列,是非循环单向队列,当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
await()和signal()就是操作等待队列,await()将线程封装到节点里面(此时,节点使用到的属性是thread prev next waitStatus),放到等待队列里面,signal()从等待队列中拿出元素。
lock()和unlock()就是操作同步队列,lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
lock方法只有一个线程的情况,如下图所示:
金手指2 公平锁加锁,只有一个线程的时候:
只有一个线程的时候,会直接调用tryAcquire,然后判断state的是不是等于0
如果等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过,通过CAS操作将state的值改成1,然后true。返回true表示加锁成功,就完成了加锁。
如果不等于0,表示不是第一次加锁,这个锁是重入锁。这个时候将原来的state值继续通过CAS操作加上1。再次返回true,表示加锁成功,就完成了加锁。需要注意的是这个时候AQS的队列没有创建出来。
注意源码中难点:
setExclusiveOwnerThread(current); // 这里是设置当前节点为独占 记住上面六个属性
看源码的时候,第一,知道自己在看什么,这里是看FairSync的lock()方法实现
第二,源码一般命名优美,可以从命名上来看,帮助理清思路
金手指:lock()是加锁
acquire()是去获得
tryAcquire()是尝试加锁
acquireQueued()是获得队列
protected final boolean tryAcquire(int acquires) { // 1、tryAcquire是去获取,2、返回为true就是使用获取的方式加锁成功(可以第一次,也可以是重入锁)
final Thread current = Thread.currentThread();
int c = getState(); // 当前状态
if (c == 0) { // 当前状态为0,就是默认状态
if (!hasQueuedPredecessors() && // 1、hasQueuedPredecessors这个方法重要,下面解释
compareAndSetState(0, acquires)) { // 1、只要上面那个hasQueuedPredecessors()返回为false,取反为true,这个cas一定是可以通过的,只是自旋等一下罢了
setExclusiveOwnerThread(current); // 1、设置当前线程为独占线程,因为当前线程已经加锁成功了,所以设置当前线程为互斥资源的独占线程
//2、为什么说当前线程加锁成功了,因为这里返回true啊
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 这句表示当前线程为独占线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true; // 1、因为当前线程是独占线程,所以一定是加锁成功,这里返回true就好
// 2、既然已经是独占线程,就没有必要再次设置当前线程为独占线程了,直接返回true
}
return false; // 1、如果既不是第一次,也不是重入锁,就不能通过获取的方式去加锁,要自己加锁,这里返回false,加锁失败
}
先知道hasQueuedPredecessors()方法是用来干什么的?
hasQueuedPredecessors()方法的意义:hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
hasQueuedPredecessors()方法的调用以及返回的意义:hasQueuedPredecessors()方法只在tryAcquire()方法里面被调用执行过,hasQueuedPredecessors()返回false表示要尝试通过获取锁的方式加锁,没有前驱节点请求获取锁,返回为true表示不需要通过获取锁的方式加锁,已经有前驱节点请求获取了。
线程加锁的流程(通过获取锁加锁的流程)是:.lock() -> .acquire() -> tryAcquire()
1、如果ht成立,h和t均为null或是同一个具体的节点,无后继节点,返回false,**表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tailheadnull**。
**key:如果类变量headtail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点**
2、如果h!=t成立(至少两个节点),head.next是否为null,如果为null,返回true。什么情况下h!=t的同时h.next==null??有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
key:此时,头结点设置为新建节点,所以head=newNode
但是,还未将头结点设置为尾节点,所以tail=null,为默认值
同时,这是第一次执行enq()方法,没有设置 node.prev = t; 和 t.next = node;,所以head.next=null。
3、如果h!=t成立(当前类变量head 和 tail不是同一个,至少两个节点),head.next != null,(true && (false || ))则判断head.next是否是当前线程,如果是当前线程(true && (false || false)),返回false,key:表示要通过获取锁的方式加锁;如果不是当前线程(true && (false || true))返回true,key:表示正在运行的线程不是当前的这个current,需要等待。
(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)实际上,hasQueuedPredecessors返回为true不通过,只需要等一段时间罢了(上面关于hasQueuedPredecessors方法的意义:如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁)
lock方法中有两个线程的情况,如下图所示:
对于上图的解释:
第一种情况:当线程B进来的时候,死循环中线程A没有解锁
我们假设线程A直接获取到了锁(获取锁的过程和上面单线程一样,不再赘言),但是线程A还没有解锁,这个时候线程B来进行加锁,走来会执行tryAcquire()方法,这个时候线程A没有解锁,所以这个tryAcquire()方法会直接返回false(state!=0,也不是重入锁),然后会调用addWaiter(Node.EXCLUSIVE)方法(新方法:上面一个线程的时候没有涉及到,这里要重点分析),这个时候会在这个方法中的enq(node)的方法中初始化AQS队列,也会利用尾插法将当前的节点插入到AQS队列中去。AQS队列如下图所示:
金手指:
注意,这时,AQS队列新建起来了
head节点没有存放线程thread=null,waitStatus=0表示什么都不是
tail节点存放的是线程B,表示什么都不是
完成AQS队列的方法是addWaiter()中调用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // 1、实际参数是Node.EXCLUSIVE,就是当前独占节点,表示下一个等待节点就是正在独占的那个线程的节点,因为它释放锁就要到插入了,所以这个方法称为addWaiter,意为添加下一个等待节点
Node node = new Node(Thread.currentThread(), mode); // 新建一个节点,存放当前线程,当前线程为内容,实参为下一个等待节点nextWaiter
Node pred = tail; // 当期尾节点赋值,当前tail==null
if (pred != null) {
node.prev = pred; // 如果不为空,进来,新建节点的前一个是之前的尾节点,就是尾插法
if (compareAndSetTail(pred, node)) { // 设置新的尾节点,从之前的尾节点pred到现在的node
pred.next = node; // 之前尾节点的next设置为这个节点
// 由此可知,尾插法三步骤:设置新节点的prev为之前尾节点、重新设置tail类变量的指向、设置之前尾节点的next为新建节点(就是三个Node类型指针而已,很简单)
return node; // 返回新建节点
}
}
enq(node); // 返回值没有接收者,但是队列新建好了
return node; // 返回这个新建的节点
}
private Node enq(final Node node) {
for (; { // 1、死循环,不创建好AQS队列不退出
Node t = tail;
if (t == null) { // Must initialize 1、第一次进入,必须初始化,这里表示连尾节点都没有
if (compareAndSetHead(new Node())) // for+if(cas)就是线程同步
tail = head; // 1、新建一个节点,设置为头结点,因为只有一个节点,所以尾节点也是这个节点
} else {
node.prev = t; // 1、这是时候,第二次循环,因为head tail都是新节点,第二次循环中使用 Node t = tail;将t设置为这个新节点
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示设置尾节点,自旋,知道设置成功 for+if(cas)就是线程同步,设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
t.next = node; // 尾节点指向参数node,头结点还是指向t
// 由此可知,尾插法三步骤:设置参数节点的prev为之前尾节点t、重新设置tail类变量的指向从之前的t到参数节点node、设置之前尾节点t的next为参数节点node(就是三个Node类型指针而已,很简单),最后队列两个元素 t 和 node
return t; // 返回头结点t
}
}
}
}
head和tail是类变量,类似指针,指向其他节点
compareAndSetTail(t, node) // 设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
compareAndSetHead(t, node) // 设置tail类变量,将head从t变为node,所以传入参数node是尾节点
compareAndSetState(0, acquires) // 设置state类变量,从0到1,cas保证安全
这个方法enq和addWaiter返回当前的节点,然后调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(金手指:返回的刚刚在addWaiter()方法中新建的最尾巴的节点作为acquireQueued方法的参数,arg参数是1,传递过来的)。这个方法中是一个死循环,由于线程A没有释放锁(tryAcquire()方法会直接返回false(state!=0,也不是重入锁)),会执行shouldParkAfterFailedAcquire(p, node)(p表示线程B节点的上一个节点,p = node.predecessor();就是最尾巴节点上一个,node表示线程B的节点,就是addWaiter()方法中新建的最尾巴的节点)第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1(执行最后一个else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,这个时候的AQS队列如下图:
金手指:
第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false
AQS队列新建起来了
head节点没有存放线程thread=null,waitStatus=-1表示后面节点的线程需要释放
tail节点存放的是线程B,表示什么都不是
和上一次的改变是:
线程B节点的上一个节点的waitStatus的值改成-1
然后第二次进入这个方法的时候,会返回true( if (ws == Node.SIGNAL) return true;),会执行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),这个时候线程B就会被park在这。(1、直到线程A解锁了,第二种情况可以当做第一个情况后面的执行来看)
第二种情况:死循环中线程A已经解锁了
上面的情况都是在线程A没有解锁的时候,如果在死循环中线程A已经解锁了。这个时候判断线程B节点的上一个节点是不是头结点,如果是的话,直接执行tryAcquire(),将当前线程B设置成独占线程,同时将state的值通过CAS操作设置成1,如果成功的话,直接返回true。表示加锁成功。这个时候会执行这个if判断中代码。执行setHead(node),这个时候AQS队列如下图:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴节点,
p.next = null; // help GC 看Java四种引用就知道 前面那个节点的next设置为null
failed = false; // 局部变量failed初始为true,要下去执行cancelAcquire,这里设置为false,不执行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 类变量head指向addWaiter的尾巴节点
node.thread = null; // 这个节点thread=null
node.prev = null; // 这个节点prev==null 因为要变成头结点,非循环双向链表,所以前驱指针为null
}
这个时候原来的线程B节点出队列,然后永远会维护一个头结点中thread为null的AQS队列。懂了,因为B节点要去执行了
金手指:公平锁加锁,只有两个线程的时候
线程A执行
lock方法中有三个线程情况,如下图:
三个线程和两个线程的情况是差不多的。金手指:加锁成功的节点永远是头结点的下一个节点中的线程加锁成功,因为是公平锁。
非公平锁会走来直接尝试加锁,如果加锁成功,直接执行线程中的代码,如果加锁不成功,直接走公平锁的逻辑。
非公平锁总结流程图如下:
讲完了加锁的过程,我们再来看看解锁的过程。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
(1)当前线程不在AQS队列中,执行tryRelease()方法。
如果当前线程不是重入锁(即当前线程不是独占的那个线程,if (Thread.currentThread() != getExclusiveOwnerThread()) 判断为ture),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
如果当前线程加的是重入锁(if判断为false,跳过),解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。
(2)AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。
(3)AQS队列中不止一个头结点,还有其他节点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于-1,if判断成立,会执行unpark的方法。unpark方法中会unpark头结点的下一个节点,然后如果当前的节点的状态是取消的状态,会从最后一个节点开始找,找到当前节点的下一个不是取消状态的节点进行unpark。这个时候也会直接返回true。表示解锁成功。
private void unparkSuccessor(Node node) { // 同步队列中的头结点head传递过来
int ws = node.waitStatus; // 为 -1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 设置状态 node的ws变为0
Node s = node.next; // 找到工作队列的head的后面一个节点
if (s == null || s.waitStatus > 0) { // head后面这个节点为空,或者waitStatus大于0
s = null; // 如果是因为waitStatus大于0而进入这个if,设置head后面的这个节点为null
for (Node t = tail; t != null && t != node; t = t.prev) // 从尾巴开始遍历,布局变量为t 没有遍历完或没找就继续找
if (t.waitStatus <= 0) // 如果t.waitStatus <= 0,将这个t记录到s,侯勇unpark用 找到当前节点的下一个不是取消状态的节点进行unpark
s = t;
}
if (s != null) // head后面的节点不为空,直接对head后面这个节点unpark
LockSupport.unpark(s.thread);
}
金手指:第二个if是如何判断三情况的:
if (h != null && h.waitStatus != 0)
第一种情况,没有AQS队列,head一定为null,就是return true;
第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;
tryLock()方法和lock()方法是差不多,tryLock方法,尝试加锁不成功后就直接返回false,具体的代码如下:
tryLock(long timeout, TimeUnit unit)方法,加了一个获取锁的时间,如果这个时间内没有获取到锁,直接返回false,表示加锁失败。
走来会尝试加锁,如果成功,直接表示加锁成功,如果不成功会执行doAcquireNanos()方法,走来先将当前节点用尾插法的方式插入到AQS队列中去,如果AQS队列没有初始化,直接初始化,将当前的节点放入到尾结点中去。然后进入死循环,这个时候判断当前节点的上一个节点是不是头结点,再次尝试加锁,如果成功直接返回true,如果失败将当前的节点的线程直接park指定的时间,当时间到了直接唤醒。再次尝试获取锁,如果成功直接返回true,如果失败直接返回false,这个方法中是可以直接响应中断的。
lockInterruptibly和lock的区别:lockInterruptibly是会立即响应中断的,并且在park中的线程也会interruptibly唤醒的,因为这个时候返回true,直接抛出异常,响应对应的中断。
而lock是要等线程执行完才会响应中断,是因为park中线程被中断唤醒后,没有抛出异常,只是将中断的标志设置成了true,等到获取到锁,执行完,才会响应对象的中断
面试问题:lock机制是如何实现公平锁的加锁和解锁的(因为synchronized无法实现公平锁)
核心语言组织:AQS内部类Node:
AQS本质是一个非循环的双向链表(也可以称为队列),所以它是由一个个节点构成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node为基本元素操作的,那么在这个Node类中需要保存什么信息呢?
注意:对于Node节点,属性包括七个(重点是前面五个)
volatile int waitStatus; //当前节点等待状态
volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //节点中的值
Node nextWaiter; //下一个等待节点
//指示节点共享还是独占,默认初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
**处在同步队列中使用到的属性(加锁、解锁)**包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
**处在等待队列中使用到的属性(阻塞、唤醒)**包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
AQS队列是工作队列、同步队列,是非循环双向队列,当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
等待队列,是非循环单向队列,当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
await()和signal()就是操作等待队列,await()将线程封装到节点里面(此时,节点使用到的属性是thread prev next waitStatus),放到等待队列里面,signal()从等待队列中拿出元素。
lock()和unlock()就是操作同步队列,lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
为什么负责同步队列的head和tail在AbstractQueuedSynchronizer类中,但是负责等待队列的firstWaiter和lastWaiter在ConditionObject类中?
解释:线程同步互斥是直接通过ReentrantLock类对象 lock.lock() lock.unlock()实现的,而ReentrantLock类对象是调用AQS类实现加锁解锁的,所以负责同步队列的head和tail在AbstractQueuedSynchronizer类中;
线程阻塞和唤醒是通过ReentrantLock类对象lock.newCondition()得到一个对应,condition引用指向这个对象,然后condition.await() condition.signal()实现的,所以负责等待队列的firstWaiter和lastWaiter在ConditionObject类中。
核心语言组织:公平锁加锁,只有一个线程的时候:
只有一个线程的时候,会直接调用tryAcquire,然后判断state的是不是等于0:
如果等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过,通过CAS操作将state的值改成1,然后true。返回true表示加锁成功,就完成了加锁。
如果不等于0,表示不是第一次加锁,这个锁是重入锁。这个时候将原来的state值继续通过CAS操作加上1。再次返回true,表示加锁成功,就完成了加锁。
需要注意的是这个时候AQS的队列没有创建出来。
注意源码中难点: setExclusiveOwnerThread(current); // 这里是设置当前节点为独占
记住上面六个属性
附加-应对面试官问题的解释:方法1:tryAcquire()方法
protected final boolean tryAcquire(int acquires) { // 1、tryAcquire是去获取,2、返回为true就是使用获取的方式加锁成功(可以第一次,也可以是重入锁)
final Thread current = Thread.currentThread();
int c = getState(); // 当前状态
if (c == 0) { // 当前状态为0,就是默认状态
if (!hasQueuedPredecessors() && // 1、hasQueuedPredecessors这个方法重要,下面解释
compareAndSetState(0, acquires)) { // 1、只要上面那个hasQueuedPredecessors()返回为false,取反为true,这个cas一定是可以通过的,只是自旋等一下罢了
setExclusiveOwnerThread(current); // 1、设置当前线程为独占线程,因为当前线程已经加锁成功了,所以设置当前线程为互斥资源的独占线程
//2、为什么说当前线程加锁成功了,因为这里返回true啊
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 这句表示当前线程为独占线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true; // 1、因为当前线程是独占线程,所以一定是加锁成功,这里返回true就好
// 2、既然已经是独占线程,就没有必要再次设置当前线程为独占线程了,直接返回true
}
return false; // 1、如果既不是第一次,也不是重入锁,就不能通过获取的方式去加锁,要自己加锁,这里返回false,加锁失败
}
附加-应对面试官问题的解释:先知道hasQueuedPredecessors()方法是用来干什么的?
hasQueuedPredecessors()方法的意义:hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
hasQueuedPredecessors()方法的调用以及返回的意义:hasQueuedPredecessors()方法只在tryAcquire()方法里面被调用执行过,hasQueuedPredecessors()返回false表示要尝试通过获取锁的方式加锁,没有前驱节点请求获取锁,返回为true表示不需要通过获取锁的方式加锁,已经有前驱节点请求获取了。
线程加锁的流程(通过获取锁加锁的流程)是:.lock() -> .acquire() -> tryAcquire()
1、如果ht成立,h和t均为null或是同一个具体的节点,无后继节点,返回false,**表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tailheadnull**。
**key:如果类变量headtail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点**
2、如果h!=t成立(至少两个节点),head.next是否为null,如果为null,返回true。什么情况下h!=t的同时h.next==null??有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
key:此时,头结点设置为新建节点,所以head=newNode
但是,还未将头结点设置为尾节点,所以tail=null,为默认值
同时,这是第一次执行enq()方法,没有设置 node.prev = t; 和 t.next = node;,所以head.next=null。
3、如果h!=t成立(当前类变量head 和 tail不是同一个,至少两个节点),head.next != null,(true && (false || ))则判断head.next是否是当前线程,如果是当前线程(true && (false || false)),返回false,key:表示要通过获取锁的方式加锁;如果不是当前线程(true && (false || true))返回true,key:表示正在运行的线程不是当前的这个current,需要等待。
(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)实际上,hasQueuedPredecessors返回为true不通过,只需要等一段时间罢了(上面关于hasQueuedPredecessors方法的意义:如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁)
核心-面试语言组织1(第一种情况:当线程B进来的时候,死循环中线程A没有解锁):
我们假设线程A直接获取到了锁(获取锁的过程和上面单线程一样,不再赘言),但是线程A还没有解锁,这个时候线程B来进行加锁,走来会执行tryAcquire()方法,这个时候线程A没有解锁,所以这个tryAcquire()方法会直接返回false(state!=0,也不是重入锁),然后会调用addWaiter(Node.EXCLUSIVE)方法(新方法:上面一个线程的时候没有涉及到,这里要重点分析),这个时候会在这个方法中的enq(node)的方法中初始化AQS队列,也会利用尾插法将当前的节点插入到AQS队列中去。
附加-应对面试官问题的解释:
完成AQS队列的方法是addWaiter()中调用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // 1、实际参数是Node.EXCLUSIVE,就是当前独占节点,表示下一个等待节点就是正在独占的那个线程的节点,因为它释放锁就要到插入了,所以这个方法称为addWaiter,意为添加下一个等待节点
Node node = new Node(Thread.currentThread(), mode); // 新建一个节点,存放当前线程,当前线程为内容,实参为下一个等待节点nextWaiter
Node pred = tail; // 当期尾节点赋值,当前tail==null
if (pred != null) {
node.prev = pred; // 如果不为空,进来,新建节点的前一个是之前的尾节点,就是尾插法
if (compareAndSetTail(pred, node)) { // 设置新的尾节点,从之前的尾节点pred到现在的node
pred.next = node; // 之前尾节点的next设置为这个节点
// 由此可知,尾插法三步骤:设置新节点的prev为之前尾节点、重新设置tail类变量的指向、设置之前尾节点的next为新建节点(就是三个Node类型指针而已,很简单)
return node; // 返回新建节点
}
}
enq(node); // 返回值没有接收者,但是队列新建好了
return node; // 返回这个新建的节点
}
private Node enq(final Node node) {
for (; { // 1、死循环,不创建好AQS队列不退出
Node t = tail;
if (t == null) { // Must initialize 1、第一次进入,必须初始化,这里表示连尾节点都没有
if (compareAndSetHead(new Node())) // for+if(cas)就是线程同步
tail = head; // 1、新建一个节点,设置为头结点,因为只有一个节点,所以尾节点也是这个节点
} else {
node.prev = t; // 1、这是时候,第二次循环,因为head tail都是新节点,第二次循环中使用 Node t = tail;将t设置为这个新节点
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示设置尾节点,自旋,知道设置成功 for+if(cas)就是线程同步,设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
t.next = node; // 尾节点指向参数node,头结点还是指向t
// 由此可知,尾插法三步骤:设置参数节点的prev为之前尾节点t、重新设置tail类变量的指向从之前的t到参数节点node、设置之前尾节点t的next为参数节点node(就是三个Node类型指针而已,很简单),最后队列两个元素 t 和 node
return t; // 返回头结点t
}
}
}
}
核心-面试语言组织2(第一种情况:当线程B进来的时候,死循环中线程A没有解锁):
第一次调用acquireQueued()方法,返回false: 这个方法enq和addWaiter返回当前的节点,然后调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(金手指:返回的刚刚在addWaiter()方法中新建的最尾巴的节点作为acquireQueued方法的参数,arg参数是1,传递过来的)。这个方法中是一个死循环,由于线程A没有释放锁(tryAcquire()方法会直接返回false(state!=0,也不是重入锁)),会执行shouldParkAfterFailedAcquire(p, node)(p表示线程B节点的上一个节点,p = node.predecessor();就是最尾巴节点上一个,node表示线程B的节点,就是addWaiter()方法中新建的最尾巴的节点)第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1(执行最后一个else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,这个时候的AQS队列如下图:
金手指:
第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false
AQS队列新建起来了
head节点没有存放线程thread=null,waitStatus=-1表示后面节点的线程需要释放
tail节点存放的是线程B,表示什么都不是
和上一次的改变是:
线程B节点的上一个节点的waitStatus的值改成-1
核心-面试语言组织3(第一种情况:当线程B进来的时候,死循环中线程A没有解锁):
第二次调用acquireQueued()方法,返回true,阻塞,直到线程A解锁: 然后第二次进入这个方法的时候,会返回true( if (ws == Node.SIGNAL) return true;),会执行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),这个时候线程B就会被park在这。(1、直到线程A解锁了,第二种情况可以当做第一个情况后面的执行来看)
核心-面试语言组织(当线程B进来的时候,死循环中线程A已经解锁):
如果在死循环中线程A已经解锁了,这个时候判断线程B节点的上一个节点是不是头结点,如果是的话,直接执行tryAcquire(),将当前线程B设置成独占线程,同时将state的值通过CAS操作设置成1,如果成功的话,直接返回true。表示加锁成功。这个时候会执行这个if判断中代码。执行setHead(node),这个时候AQS队列如下图:
这个时候原来的线程B节点出队列,然后永远会维护一个头结点中thread为null的AQS队列。懂了,因为B节点要去执行了
附加-应对面试官问题的解释:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴节点,
p.next = null; // help GC 看Java四种引用就知道 前面那个节点的next设置为null
failed = false; // 局部变量failed初始为true,要下去执行cancelAcquire,这里设置为false,不执行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 类变量head指向addWaiter的尾巴节点
node.thread = null; // 这个节点thread=null
node.prev = null; // 这个节点prev==null 因为要变成头结点,非循环双向链表,所以前驱指针为null
}
核心语言组织-线程解锁的三种情况:
(1)当前线程不在AQS队列中,执行tryRelease()方法。同时当前线程不是重入锁,直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0,如果当前线程加的是重入锁。解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。
(2)AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。
(3)AQS队列中不止一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于-1,if判断成立,会执行unpark的方法。unpark方法中会unpark头结点的下一个节点,然后如果当前的节点的状态是取消的状态,会从最后一个节点开始找,找到当前节点的下一个不是取消状态的节点进行unpark。这个时候也会直接返回true。表示解锁成功。
金手指:第二个if是如何判断三情况的:
if (h != null && h.waitStatus != 0)
第一种情况,没有AQS队列,head一定为null,就是return true;
第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;
原理层面:ReentrantLock中lock()与unlock()(即AQS加锁解锁),完成了。
天天打码,天天进步!!!