AQS是JAVA中的一组抽象类,就是为了解决多线程并发竞争共享资源而引发的线程安全问题,细致点说AQS就是具备一套线程阻塞等待以及被唤醒的时候锁分配的机制,这个机制是由队列来实现的,暂时获取不到所的线程加入到队列里面,AQS本身并没有实现太多的业务功能,只是对外提供了三点核心内容来帮助实现其他的并发内容
拿ReenTranLock来举例,是否可以获取到互斥锁,完全取决于是否可以基于CAS将state从0改成1,如果获取锁成功,那么可以执行代码,如果没有获取到锁成功,那么直接加入到双向链表,如果持有锁的线程执行了await()方法,那么当前持有锁的线程释放锁,并进入到单向链表里面;
1)由int类型修饰的state属性,由volitaile修饰,基于CAS修改的核心属性
比如说Reentranlock和ReentrankReadWriteLock获取到的所的方式都是针对于state变量做修改来实现的,state等于1代表有线程获取到锁,CountDown计数器和Semphore计数器赋初值就是根据state来赋值的,state代表计数器;
2)由Node对象组成的双向链表,比如说ReentranLock有一个线程没有拿到锁资源,需要将线程封装成Node对象,并加Node对象加入到双向链表中,然后将线程挂起,进行阻塞等待;
3)由Node对象组成的单向链表,实现线程等待以及唤醒的Condition,叫做ConditionObject,比如说ReentranLock,一个线程持有锁,执行了await()方法,此时这个线程就被封装成Node对象,并且被添加到单向链表里面,况且会主动释放锁,此时这个线程没有竞争锁的资格;
上面的这两个链表都是存放Node对象的,Node对象都是存放有正常的线程信息的,wait和Condition.await()会自动释放锁;
await方法:将持有锁的线程释放锁,封装成Node节点,加入到Condition双向链表中
signal方法:将Condition单向链表中的Node唤醒同时加入到AQS双向链表
4)一个AQS组件只能有一个双向链表,但是确实可以有多个单向链表,因为一个lock锁可以产生多个Condition类,如果执行了Condition1.await()方法,那么就加入到Condition1的双向链表里面,如果执行了Condition2.await()方法,加入到Condition2的双向链表里面
1)当new了一个ReenTranLock的时候,AQS默认就是head=tail=null,state等于0
2)此时来了一个A线程,执行lock方法获取锁资源,CAS操作将state变成1,获取锁成功,A线程持有锁资源;
3)B尝试获取到锁资源,B线程尝试获取到锁资源,但是锁资源被A资源占用,先创建一个Node节点作为傀儡节点也就是头节点,然后将当前这个失败的线程封装成一个Node,加入到这个傀儡节点的后面
4)挂起B线程,当前有一个ws属性,如果ws是-1,表示后面节点被挂起,等到A线程释放锁资源将state变成0然后去唤醒B线程,唤醒head.next
5)B线程就可以尝试重新获取到锁资源
ReenTranLock没有直接继承AQS,当执行到lock方法的时候发现执行了sync的lock方法,sync是一个抽象类,继承了AQS,Sync有两个子类实现一个是公平锁,一个是非公平锁
FairSync NonFairSync
//非公平锁的lock实现 final void lock() { //线程到达以后直接尝试将state从0改成1,成功就拿到锁资源 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else//如果if中的CAS失败执行acquire方法直接走后续操作 acquire(1); } //公平锁的lock实现 final void lock() { acquire(1); }
一)acquire方法:
public final void acquire(int arg) { //1.查看tryAcquire方法,尝试再次去重新获取到锁,如果这个方法返回的是true,那么直接后面逻辑都不用走了 //2.查看addWaiter:没有获取到锁,要去排队了 //3.查看acquireQueued:挂起线程和后续被唤醒继续锁资源的逻辑 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
一)AQS的tryAcquire方法没有任何实现,必须被Reentranlock中的公平锁和非公平锁实现重写方法
非公平锁的实现:
1)没有线程持有锁,尝试获取锁
2)有线程获取锁,判断是不是可重入锁
3)如果上面两块都失败,直接返回false
final boolean nonfairTryAcquire(int acquires) { //1.获取到当前竞争锁失败的线程,获取到锁对象 final Thread current = Thread.currentThread(); int c = getState(); //2.然后再来进行判断公共状态state也就是c,然后再次尝试获取到锁 //state=0代表当前资源没有被锁住,此时当前线程可以尝试加锁占有资源 if(c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);//将一个线程赋值给当前ReenTrankLock中的互斥锁拥有者的线程对象的属性 return true; } } //3.此时获取到锁还是失败,锁某一个线程持有判断当前获取锁失败的线程是都等于当前之前获取锁成功的线程,就是判断是否是可重入锁,当前线程是独占线程那么每一次就+1 //state值不等于0 判断当前线程和持有当前资源线程是不是同一个线程,是,那就是可重入锁逻辑,就累加 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires=1; if (nextc < 0) // 防止加满int溢出 throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
公平锁的实现:
1)hashQueedPredecessors()方法判断,判断自己是不是第一个
2)如果当前是队列的首元素,直接CAS尝试获取到锁compareAndSetState
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //虽然锁资源没有被持有,但是需要判断有没有别的线程正在排队; if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //直接将拥有锁的线程设置为自己 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } } protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }
二)addWaiter方法:将当前线程封装成Node对象,并且插入到AQS的双向链表
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail;//当前队列尾节点是否存在 if (pred != null) {//当前队列没有进行初始化 node.prev = pred;//如果尾巴节点存在,直接插入到尾巴节点的后面 if (compareAndSetTail(pred, node)) { //CAS设置尾节点,CAS(tail,pred,node) pred.next = node; return node; } } enq(node);//创建队列 return node; }
//这个代码是为了制定假设有若干个线程同时执行入队操作况且此时队列仍然是null private Node enq(final Node node) { for (;;) {//死循环确保这个对应的节点一定可以入队 Node t = tail; if (t == null) { //如果尾巴节点等于null if (compareAndSetHead(new Node()) //需要注意的是,这里不是用的方法参数node,而是先创建了一个Node,并且head,tail都指向了这个空Node //并发环境下设置头节点CAS(head,null,new Node()) //队列只是需要一个线程创建就可以了,后续的线程直接插入到队列的结尾即可 tail = head;//设置完成头节点,当前节点也是尾巴节点=null } else { //执行入队操作,如果第一个线程创建队列成功,然后再走一次循环保证入队成功 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
三)acquireQueued方法
如果获取锁失败,先创建队列让自己入队,然后将前驱节点的waitStatus变成-1,这就意味着是如果前驱节点释放锁,就可以立即通知到我,最后直接调用if语句中后面的parkAndCheckInterrupt方法,lockSupport(this)将自己阻塞
解锁:将state变成0,将ExclusiveOwnerThread变成null,唤醒队列中阻塞的线程
将Node添加到AQS之后的操作
ws:0表示默认值
ws:1节点取消了
ws:-1代表当前节点的next节点挂起了,将来我这个节点执行工作之后释放锁了,然后直接唤醒下一个节点;
ws:-2放到Condition队列中的节点必须是-2;
final boolean acquireQueued(final Node node, int arg) { boolean failed = true;//标记是否成功拿到资源 try { for (;;) {//代表我要拿的锁资源,拿不到就一直死循环 //1.先拿到前驱节点,队列中有很多节点,只有排在第一位的节点才可以去执行尝试拿锁 final Node p = node.predecessor(); //第二次tryAcquire,也就是在入队列前再尝试一把 //2.如果上一个节点是head代表当前当前节点排在第一个位置 if (p == head && tryAcquire(arg)) {//进来之后代表拿到锁了 setHead(node); //3.把当前节点设置成null,当前节点的线程属性是空,前驱节点也是空 p.next = null; //前一个结点也变成null interrupted=false //成功获取资源,进行返回 return interrupted; } //4.要么当前节点的前驱节点不是head,要么获取到锁失败了,反正node没拿到锁 //a.shouldPark()挂起当前线程 //parkAndCheckInterrupted()挂起当前线程等待被唤醒再来获取锁资源 if (shouldParkAfterFailedAcquire(p, node)&&parkAndCheckInterrupt()( ) interrupted = true; } } finally { if (failed) // 如果上面代码没有获取锁报错,需要取消获取锁的动作 cancelAcquire(node); } } //是挂起线程的准备方法 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //走进这个方法说明前面没有获得锁,先拿到当前节点的前序节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果上一个节点的状态是-1,代表可以挂起Node线程,也代表着上一个节点可以唤醒后续当前的节点 return true; //下面的情况,node节点不需要去park,最终返回false使上层调用方法死循环直到获取锁首先是ws>0 if (ws > 0) { //如果先序节点的状态是取消,则把异常的先序节点从队列中删除,头节点要么是0要么是-1 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0);//向前找到前面不是要被干掉的节点 pred.next = node; } else { //说明前驱节点状态不是1,代表没有取消,将前面的节点变成-1,让当前节点被挂起 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
四)tryRealse()方法释放锁:直接解锁来释放锁,将当前全局持有锁线程置为null
解锁完成之后还会唤醒后续阻塞的线程
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0)//队列不为空况且队列的头部不为空 unparkSuccessor(h);//尝试唤醒head后面的第一个节点 return true; } return false; }
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);//this.state=0 return free; }
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0)//小于0直接将waitStatus变成0 compareAndSetWaitStatus(node, ws, 0); Node s = node.next;//找到下一个要唤醒的节点 if (s == null || s.waitStatus > 0) {//如果下一个不是无效节点就继续向后找 s = null;找一个符合条件,即真正在阻塞睡眠的线程 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);//尝试使用该方法唤醒该节点 }
五)AQS中的Condition类
Condition就是AQS中的一个内部类,await方法和signal方法
1)await挂起线程的流程
2)signal唤醒线程的流程
3)await线程被唤醒后的逻辑是什么?
public final void await() throws InterruptedException { //1.如果线程已经被中断了,直接抛异常啥也不干 if (Thread.interrupted()) throw new InterruptedException(); //2.把当前线程封装成一个Node节点存放到单向链表里面 Node node = addConditionWaiter(); //3.释放当前线程拥有的锁资源,充分释放就是避免可重入锁 int savedState = fullyRelease(node); int interruptMode = 0; //5.是否这个节点在AQS双向链表中,如果进入到这个while循环里面,代表Node没有在AQS的双向链表中,于是线程挂起 while (!isOnSyncQueue(node)) { LockSupport.park(this);//挂起当前线程 ....... } private Node addConditionWaiter() { //1.先获取到尾巴节点 Node t = lastWaiter; //2.健壮性判断,如果当前的tail节点不为空况且tail节点的状态不是-2,说明tail节点不配呆在单向链表里面 if (t != null && t.waitStatus != Node.CONDITION) { //重新调整尾巴节点 unlinkCancelledWaiters(); //给尾巴节点赋值 t = lastWaiter; } //3.创建新节点 Node node = new Node(Thread.currentThread(), Node.CONDITION); //4.将当前节点挂到Condition单向链表的末尾 if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
在AQS中,实现公平锁和非公平锁只有两点不同:
1)非公平锁在调用lock方法以后,首先这个线程上来就调用CAS来尝试获取到锁,如果这个时候恰好所没有被占用,那么就直接获取到锁直接返回了
2)非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面;
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒,相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
五)AQS的工作流程:
1)线程请求同步资源:当一个线程请求某一个同步资源也就是尝试进行加锁的时候,AQS会尝试使用CAS来操作修改同步状态,如果成功获取到了锁,该线程可以继续执行
2)获取同步状态失败:如果说当前同步状态已经被其他线程占用了,锁被其他线程获取了,那么当前线程就需要将等待,AQS就会将该线程封装成一个节点,加入到双向链表中
3)自旋和阻塞:在等待队列中的线程会不断地进行自旋尝试获取到锁,如果自旋一定次数还是获取不到锁,那么就进入到阻塞状态,等待被唤醒
4)线程释放锁:当线程完成了对资源的操作需要释放锁的时候,这个线程就会调用AQS方法中的release方法,这个线程会使用CAS来修改同步状态,并唤醒等待队列中的一个线程或者是多个线程
5)等待唤醒线程:AQS在释放资源以后,会从队列中选择一个或者是多个线程并将其唤醒,被唤醒的线程会尝试再次去获取同步状态,如果获取成功,那么继续执行,如果获取失败,那么继续进入自旋或者是阻塞状态
1)AQS也被称之为是抽象同步队列,它是JUC包底下的多个组件的底层实现,Lock,CountDownLatch和Semphore底层都使用到了AQS
AQS的核心思想就是给予一个等待队列和同步状态来实现的,它的内部使用一个先进先出的队列管理来获取同步资源的线程,每一个线程在竞争同步资源的时候会先尝试获取同步资源,如果获取不到,那么会被封装成一个节点加入到阻塞队列中
2)在底层的AQS提供了两种锁机制,分别是共享锁和排他锁
排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 可重入锁实现就是用到了 AQS 中的排它锁功能;
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和Semaphore都是用到了AQS中的共享锁功能;
AQS是抽象同步队列,就是一个抽象类,Reentranlock和信号量和CountDownLatch在底层都是基于AQS来实现的,就是实现这些产品的一个公共的方法,无论是锁,是需要竞争那一把锁的,信号量也是需要得到停车位的,计数器-1,计时器也是有多个线程来竞争同一把锁,计数器-1,多个产品是具有一个公共的功能的,于是就把这个公共的功能封装起来实现了一个抽象类
3)设计整个AQS体系需要解决的三个问题就是:
3.1)互斥变量的设计以及多线程同时更新互斥变量时候的安全性
a)AQS采用了int类型的互斥变量来记录竞锁的一个状态,0表示没有任何线程获取到锁资源,大于等于1表示已经有线程获取到了锁资源并持有锁
b)如果是无锁状态,竞争锁的线程则把这个state更新成1,表示占用到锁,此时如果多个线程进行同样的操作,会造成线程安全问题,AQS采用了CAS机制来保证互斥变量state的原子性
3.2)为竞争到的锁的线程等待以及竞争到的锁资源的线程释放锁之后的唤醒
c)未获取到锁资源的线程通过Unsafe类中的park方法对线程进行阻塞,把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从双向链表的头部去唤醒下一个等待的线程再去竞争锁;
3.3)锁竞争的公平性和非公平性
另外关于公平性和非公平性问题,AQS的处理方式是,在竞争锁资源的时候
公平锁需要判断双向链表中是否有阻塞的线程,如果有,则需要去排队等待;
非公平锁不管双向链表中是否存在等待锁的线程,都会直接尝试更改互斥变量state去竞争锁
3.4)共享锁和排他锁
AQS:
抽象层:AbstractSynchronizerQueue
state表示资源的可用状态
同步等待队列:(CAS volitaile int state)入队出队逻辑,双向链表
加锁解锁,具体这个函数是由AQS的方法声明,由子类来实现
因为不确定是由共享还是排他的
条件等待队列:也是有入队出队操作,单向链表
等待唤醒:LockSupport.park()和LockSupport.unpark();
每一次调用sigal方法就会从条件队列里面取出节点直接加入到同步队列的节点插入
setExclusiveOwner进行和当前AQS中的一个变量进行绑定,这个变量是就是当前获取到独占锁的线程,下面是不需要进行CAS的,因为加锁只有一个线程
import java.util.concurrent.locks.AbstractQueuedSynchronizer; public class MyLock extends AbstractQueuedSynchronizer { @Override //通过CAS进行加锁,state等于0 protected boolean tryAcquire(int arg) { if(compareAndSetState(0,1)){ setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int arg) { setExclusiveOwnerThread(null); setState(0); return true; } }
Thread.sleep()方法可以让线程进入到阻塞状态,让出CPU的执行权,这个方法可以传入一定的参数让线程休眠指定的时间,让CPU的执行权给到其他线程或者是进程,操作系统底层会设置一个定时器,当定时器的时间到了以后,操作系统会再次唤醒这个线程,Thread.sleep(0)虽然没有传递睡眠时长,但是还是会触发线程调度的切换,当前线程会从运行状态切换到就绪状态,然后操作系统会根据优先级选择一个线程来执行,如果有优先级更高的线程来等待时间片,那么这个线程就会得到执行,如果没有就会可能立即选择刚刚进入到就绪状态的这个线程来执行,具体的调度策略,取决于操作系统底层的调度算法
CAS保证多线程环境下共享变量操作的一个原子性
一)获取到AQS的同步状态,就相当于是获取到了锁吗?
在大多数情况下,AQS中获取到同步状态确实是表示获取到了锁资源,但是某些情况下获取到同步状态表示获取到了某一些条件,而不是锁资源;
1)当使用Reentranlock的时候,AQS的子类会确保在获取到同步状态的时候,该线程获取到了锁,并且可以继续执行临界区的代码,这种情况下,获取到了同步状态确实是获取到了锁资源
2)但是对于AQS来说,他还可以实现一些其它类型的同步器,比如说信号量和CountDownLatch,在这些场景下,获取到同步状态并不是代表着获取到了锁资源,而是获取到了特定类型的同步器所提供的信号或者是等待条件