AQS+ReentrankLock
介绍
什么是AQS?
AQS是JUC包下的一个同步队列器,他是大部分并发工具类的核心。
AQS的作用
AQS的主要作用是使用state去控制资源数,并使用队列去维护没有获取到资源的线程。他使用模版方法设计模式把操作资源的方法交给子类实现,其中最典型的实现就是ReentantLock。
AQS的应用场景
线程协作
Semaphore、CountDownLatch、(CyclicBarrier没有,内部使用lock好像**)
线程安全
ReentrantReadWriteLock、ReentrantLock
线程管理
ThreadPoolExecutor
源码分析
AQS结构
AbstractOwnableSynchronizer(AOS)
核心属性和方法
Java
Thread exclusiveOwnerThread; // 独占模式资源持有者(线程)
void setExclusiveOwnerThread(Threadthread); // set方法
Thread getExclusiveOwnerThread(); // get方法
核心属性和方法
Java
int WAITING = 1; // 常量1,表示线程等待
int CANCELLED = 0x80000000; // 常量负数,表示取消等待
int COND = 2; // 和等待队列有关
Node head;// 同步队列的头结点,空节点,不包含线程(特殊)
Node tail;// 同步队列的尾节点
int state;// 同步状态,state==1,放入同步队列中
class Node{ // 通过Node实现了同步队列,双向链表
Node prev; // 链表中的prev,volatile修饰
Node next; // 链表中的next,volatile修饰
Thread waiter; // Node包装的线程
int status; // 原子位操作,相当于状态,volatile修饰
Node SHARED = new Node(); // 共享模式
int CANCELLED = 1; // 标识线程已处于结束状态,表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化
int SIGNAL = -1; // 标识线程已处于可被唤醒状态
int CONDITION = -2; // 标识线程已处于条件状态,表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
int PROPAGATE = -3; // 与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
int waitStatus; // 同步队列中结点状态:CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种, waitStatus表示的是后续结点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。
Node nextWaiter; // 等待队列中的后继结点,这个与Condition的等待队列有关
}
class ConditionObject{}
tryAcquire(int arg) //尝试获取独占锁-模版方法(待重写)
tryRelease(int arg) //尝试释放独占锁-模版方法(待重写)
tryAcquireShared(int arg) //尝试获取共享锁-模版方法(待重写)
tryReleaseShared(int arg) //尝试释放共享锁-模版方法(待重写)
isHeldExclusively() //判断是否为持有独占锁(待重写)
setState();//没有竞争时使用,效率高一些,一般是持有锁的线程
getState();//获取状态
compareAndSetState();//由竞争的时候使用
acquire();
acquireShared() ;
acquireInterruptibly() ;
acquireSharedInterruptibly() ;
release() ;
releaseShared() ;
addWaiter();
addWaiter()
Java
private Node addWaiter(Node mode) { // 创建新节点,并加入队列
// 将请求同步状态失败的线程封装成结点
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点
Node pred = tail;
// 如果尾节点为null,说明头结点都还没初始化
if (pred != null) {
// 如果头结点被初始化,就直接把新节点加入队列(快速插入)
node.prev = pred;
//使用CAS执行尾部结点替换,尝试在尾部快速添加,如果CAS失败,执行enq
if (compareAndSetTail(pred, node)) {
//为什么CAS会失败呢?因为这时候可能有多个线程包装成的结点放入到队尾
pred.next = node;
return node;
}
}
// 把新节点加入队列,头结点不存在就初始化头结点
enq(node);
return node;
}
enq()
Java
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾节点为null,说明队列中没有节点,开始初始化头结点
if (t == null) {
// 新建一个空节点作为头结点
if (compareAndSetHead(new Node()))
// 尾指针指向头结点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued()
TypeScript
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 出循环的两种情况1、获取到锁 2、被中断?????说明新节点在队列中不会被阻塞?直到获取锁?
for (;;) {
// 获取新节点的前置节点
final Node p = node.predecessor();
// 如果是头结点,就尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功,把当前节点设为头结点,并置为null
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断挂起的两种情况
// 1、前驱结点不是head;
// 2、前驱节点是head,但是获取锁失败
if (shouldParkAfterFailedAcquire(p, node)
&& parkAndCheckInterrupt()) // 这里面有个阻塞方法LockSupport.park(this);
interrupted = true;
}
} finally {
if (failed)
//最终都没能获取同步状态,结束该线程的请求,并退出队列
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire()
TypeScript
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// pred是node的前驱节点,但是pred不是头节点,因为如果是头节点,node就不用挂起了
// 获取前驱节点的等待状态
int ws = pred.waitStatus;
// 等待唤醒状态(SIGNAL)
if (ws == Node.SIGNAL){
return true;
}
//如果ws>0 则说明是结束状态,遍历前驱结点直到找到不是结束状态的结点
if (ws > 0)
{
// 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
// 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
do
{
node.prev = pred = pred.prev;
}
while (pred.waitStatus > 0);
pred.next = node;
}
// ws小于0又不是SIGNAL状态
// 一般是从等待队列中刚转过来,是Condition状态 则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒
else
{
// //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
return false;
}
parkAndCheckInterrupt()
Java
//只有当前驱节点是唤醒状态,才挂起当前结点
private final boolean parkAndCheckInterrupt() {
//将当前线程挂起
LockSupport.park(this);
//获取线程中断状态,interrupted()是判断当前中断状态
//并非中断线程,因此可能true也可能false,返回
return Thread.interrupted();
}
问说有哪些属性,首先链表必定有头指针和尾指针,方便操作,还有节点,节点内部有值和前后指针,AQS最核心的就是state
ReentrankLock
介绍
什么是ReentrankLock?
ReentrankLock是基于AQS实现的可重入锁,有公平和非公平两种实现,其中非公平的效率比较高,比较常用。
ReentrankLock的作用是什么?
ReentrankLock的主要作用就是锁住临界区,来保证共享变量的安全(让线程在一定程度上变得有序)。
ReentrankLock应用场景
适合写多读少的需要保证共享变量安全的情况
ReentrankLock源码分析
ReentrankLock结构
核心属性和方法
Java
Sync sync;//
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();//抽象方法,子类实现
boolean nonfairTryAcquire(int acquires);//获取非公平锁,不写在非公平队列里面是因为 tryLock需要调用他
boolean tryRelease(int releases);//释放锁
boolean isHeldExclusively();//持有锁线程是否是当前线程
int getHoldCount(); // 获取重入次数
boolean isLocked(); // 判断是否被锁住
ConditionObject newCondition() ;//创建Condition队列
}
static final class NonfairSync extends Sync {
void lock();
boolean tryAcquire(int acquires); //获取锁
}
static final class FairSync extends Sync {
void lock();
boolean tryAcquire(int acquires); //获取锁
}
lock、trylock、lockInterruptibly对比
TypeScript
//-------------------------------------------- 1.lock-非公平-起点 ------------------------------------------------------------
// if (compareAndSetState(0, 1)){ -------------------------
// setExclusiveOwnerThread(Thread.currentThread()); | //非公平锁独有
// } ----------------------------------------------------
//-------------------------------------------- 2.lockInterruptibly-公平和非公平-起点 -------------------------------------------
// if (Thread.interrupted()){---------------
// throw new InterruptedException(); | //中断锁独有
// }----------------------------------------
//-------------------------------------------- 3.lock-公平-起点 --------------------------------------------------------------
//-------------------------------------------- 4.tryLock-起点 ---------------------------------------------------------------
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors公平锁独有,try没有这部分
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
}
//------------------------------------------ 4.tryLock-终点 -----------------------------------------------------------------
boolean failed = true;
try {
boolean interrupted = false;
for (;;)
{
final Node p = node.predecessor();
if (p == head && tryAcquire(arg))
{
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
// throw new InterruptedException() // 中断锁独有
}
//------------------------------------------ 1.lock-非公平-终点 -------------------------------------------------------------
//------------------------------------------ 2.lockInterruptibly-公平和非公平-终点 --------------------------------------------
//------------------------------------------ 3.lock-公平-终点 ---------------------------------------------------------------
面试题
ReentrankLock是悲观锁还是乐观锁,为什么?
lock锁局部使用的是乐观锁,总体使用的是悲观锁。
乐观锁是指不需要获取锁,直接进行操作
悲观锁是指必须要获取到锁,才能进行操作
lock锁底层使用cas去获取锁(state),所以说他局部是乐观锁
但是总整体角度上看,必须获取到lock锁,才能进入到临界区进行操作,所以说整体是一个悲观锁。
ReentrankLock和Synchronized的区别?
相同点,都是悲观锁
不同点
1、使用方式不同,一个是关键字,一个是类,关键字内部自己实现了上锁和解锁的逻辑,类必须主要调用方法进行上锁和解锁,但是灵活性会更高
2、功能不同,lock可以响应中断,可以判断是否获取到锁,有公平和非公平的实现,还可以顺序唤醒等待队列中的线程。Synchronized不可以响应中断,只有非公平实现,唤醒等待队列中的线程也是随机的。
3、底层原理不同,ReentrankLock是基于AQS,使用cas+locksupport实现的,Synchronized是基于cas+对象头+monitor实现的。
ReentrankLock常用方法有哪些?有什么区别?
常用方法有lock、中断lock、tryLock。
他们的核心逻辑都是一样的,都会去判断锁是否被持有,没有被持有就用cas尝试去获取锁。如果锁被人持有,就会判断持有线程是否是当前线程,是的话就重入。这个这三种方法相同的逻辑。
不同的是,lock和中断lock在没获取到锁之后,都会新增节点,进入阻塞队列。而tryLock会直接返回一个布尔值。
重点是lock和中断lock的区别。
lock和lockInterruptibly有什么区别?为什么lockInterruptibly可以被中断?
他们都是锁,不过中断锁再没获取到锁阻塞的时候,可以打断阻塞,并抛出中断异常,退出阻塞队列。lock锁也可以响应中断,因为底层使用的都是lockSupport,但是他只会给一个中断标记,然后继续循环,继续阻塞。
在非公平的情况下,中断锁还比lock锁少了一次获取锁。公平锁情况下基本是一致的。
谈一谈你对AQS的理解
AQS是JUC包下的一个同步队列器,他是大部分并发工具类的核心。他的主要作用是使用state去控制资源数,并使用队列去维护没有获取到资源的线程。他使用模版方法设计模式把操作资源的方法交给子类实现,其中最典型的实现就是ReentantLock。
哪些并发工具类是通过AQS实现的?
根据线程协作,有Semaphore和CountDownLatch
根据线程安全,有ReentrantLock和ReentrantReadWriteLock
根据线程管理,有线程池
JUC下都有哪些并发工具类?
Atomic原子类,并发集合类,并发队列,线程协作类,线程池
读写锁,也分为公平和非公平
state拆两半,低16位,用来记录写锁,高16,读,代表重入次数
拆两半不用两个变量是因为无法用一次CAS 同时操作两个int变量
state=0说明没有读锁也没有写锁,如果>0就判断是读锁还是写锁