闲来不卷,随便聊一点。
创新互联建站主要从事网页设计、PC网站建设(电脑版网站建设)、wap网站建设(手机版网站建设)、自适应网站建设、程序开发、网站优化、微网站、小程序设计等,凭借多年来在互联网的打拼,我们在互联网网站建设行业积累了丰富的成都网站设计、成都做网站、网站设计、网络营销经验,集策划、开发、设计、营销、管理等多方位专业化运作于一体。
一般情况下,大家系统中至少也是JDK8了,那想必对于JDK5加入的一系列功能并不陌生吧。那时候重点加入了java.util.concurrent并发包,我们简称JUC。JUC下提供了很多并发编程实用的工具类,比如并发锁lock、原子操作atomic、线程池操作Executor等等。下面,我对JUC做了整理,大致分为下面几点:
基于JDK8,今天重点来聊下JUC并发包下的一个类,AbstractQueuedSynchronizer。
首先,浅显的从名字上看,抽象的队列同步器;实际上,这名字也跟它的作用如出一辙。抽象,即需要被继承;队列同步器,其内部维护了一个队列,供线程入队等待;最终实现多个线程访问共享资源的功能。
进入AbstractQueuedSynchronizer内部,需要掌握三个重要的属性:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
我们调试AQS的源码,必须寻找一个源码调试的切入点,我这里用我们并发编程常用的Lock锁作为调试AQS的切入点,因为这是解决线程安全问题常用的手段之一。
AQS的源码调试,从Lock接口出发,JDK源码定义如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
从源码中看到,Lock是一个接口,所以该接口会有一些实现类,其中有一个实现类ReentrantLock,可重入锁,想必大家都不会陌生。
通过跟踪源码可以看到,ReentrantLock#lock内部实现貌似比较简单,只有简短的一行代码
public void lock() {
sync.lock();
}
其实内部是维护了一个Sync的抽象类,调用的是Sync的lock()方法。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
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);
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// ...
}
可以看到,Sync也是个抽象类,它有两个实现类:NonfairSync和FairSync,这里其实就引出了我们今天的主角,AbstractQueuedSynchronizer,Sync继承了它。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
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;
}
}
下面我整理了这一系列类的UML图
通过类图可知,lock()方法最终调用的是ReentrantLock类下,内部类NonfairSync或FairSync的lock方法;对于这两个类,前者叫非公平锁,后者叫公平锁。通过ReentrantLock的构造器可知,默认使用NonfairSync类。
public ReentrantLock() {
sync = new NonfairSync();
}
从NonfairSync类的lock方法出发,引出第一个AQS下的方法compareAndSetState。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
从compareAndSetState方法的命名可以发现,就是比较并交换的意思,典型的CAS无锁机制。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
我们可以观察到,这里其实调用的是Unsafe类的compareAndSwapInt方法,传入的expect为0,update为1;意思是如果当前值为0,那我就把值最终更新为1。
Unsafe这个类下面,发现好多方法都是用native这个关键词进行修饰的(也包括compareAndSwapInt方法),用native关键词修饰的方法,表示原生的方法;原生方法的实现并不是Java语言,最终实现是C/C++;这并不是本文的讨论范围。
回到AQS的compareAndSetState方法,返回值是boolean类型,true表示值更新为1成功,false表示不成功。这里出现两个分支,成功,走setExclusiveOwnerThread方法;不成功,走acquire方法。咱优先讨论acquire方法。
先来看一下该方法的源码;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里的核心是两个方法,tryAcquire方法和acquireQueued方法。首先会调用tryAcquire()方法,看方法命名是尝试获取;实际上这个方法确实就在做一件事“尝试获取资源”。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
不过AQS中的这个方法是protected修饰,并没有去实现,仅仅只是预留了方法入口,后期需要由其子类去实现;这里的子类就是上文中的NonfairSync类,该类的源码在上文中已经贴出。这段源码其实运用了我们常见的一个设计模式,“模板方法模式”。
NonfairSync的tryAcquire方法源码如下
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这里并没有直接去实现tryAcquire方法,而是调用了Sync类下的nonfairTryAcquire方法。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里有个getState方法,最终返回的是AQS中的state字段,这个字段就是多个线程抢占的共享资源,所以这个字段很重要;volatile关键字修饰,保证内存的可见性,int类型,对于ReentrantLock锁而言,当state=0时,表示无锁,当state>0时,表示资源已被线程锁定。
下面分析下这段代码:
tryAcquire方法的判断至此结束,不过最终的走向需要看它的返回值;返回true,表示当前线程抢占到锁,或者当前线程就是抢占锁的线程,直接重入,加锁流程结束;返回false,表示没有抢占到锁,流程继续,这里就引出下个话题,CLH线程等待队列。
首先咱来看一段源码中的注释
The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks
大致意思是:CLH队列是由Craig、Landin、Hagersten这三位老哥名字的首字母叠加在一起命名的,它是一个等待队列,它是一个变种队列,用到了自旋。
这里的信息要抓住三点:等待队列、变种队列、自旋。
在解析addWaiter方法实现之前,就不得不提到一个内部类Node;addWaiter方法的入参是这个类型,所以先来看看这个类。源码如下:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
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;
}
Node() {
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
这里先大致介绍下,每个属性的意思:
另外,Node类还有两个有参构造器:从作者的注释就能看出来,第一个构造器是在等待队列的时,创建节点使用,第二个构造器是在条件队列时,创建节点使用。
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)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
其实这段方法是在创建Node对象,Node对象就是组成CLH队列的基础元素。
总结下addWaiter方法干的事情:
还是先来看下这个方法的源码;
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
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)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
从这个方法看到,又是运用了无限循环,需要分两个步骤去观察:1.当前方法中的判断,自己的上一个节点是否是头部节点(头部节点就是占用资源的节点);2.当前节点正式入队列,并且被挂起。
当前节点的前一个节点是队列头部,意味着当前节点的前一个节点,就是持有资源的节点;当资源被释放,当前节点会去尝试争夺锁资源;如果拿到锁资源,当前节点会被标记为队列头部节点,它的上个节点(老的头部节点)会被置为null,需要被GC及时清除,所以作者在这里添加了一个注释:help GC;下图就是描述了这个流程:
如果当前节点的上一个节点,并不是头部节点;这里就需要用到上述Node类中介绍的各种状态字段了;先来重点介绍下Node类中的两个状态属性:
进入的shouldParkAfterFailedAcquire这个方法内部,该方法接受两个参数:当前节点前一个节点和当前节点。首先,获取上一个节点的waitStatus属性,然后通过这个属性做如下判断:
这里可以想象成一个排队去食堂打饭的场景,你在低头玩手机前,跟你前面的同学说,我玩会手机,快到了叫我一下;结果你前面的同学嫌队伍长走了(CANCELLED状态),所以你只能继续找他的上一个同学;直到有同学回答你,好的(该同学被标记SIGNAL状态);然后你就低头玩手机,等待回答你“好的”的那个同学叫你。
再来看下parkAndCheckInterrupt这个方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// LockSupport#park
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
其中最终又是这个Unsafe类,通过它的原生方法park,去挂起当前线程,这里就不展开赘述了。
下面整理下从lock方法作为切入点,一系列的调用:
之前一直在讲资源“上锁”,那么这个方法就是给资源解锁。这里给出重要的部分源码
// AQS中
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// AQS中
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 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);
}
// ReentrantLock中
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;
}
在调用unlock方法去解锁后,最终是调用AQS中的release方法去实现这个解锁功能的;在该方法中,首先会调用ReentrantLock中的tryRelease方法,去做state状态值的递减操作。
在tryRelease方法返回false的时候,release方法并不会做任何操作,直接就结束了,意味着解锁并没有完成;但是在返回true的时候,具体分以下几部操作:
上面说到了,这个方法主要是用来唤醒线程的,下面还是做一下具体的解析:
通过上面的描述可以发现,资源解锁是相对简单的;它只能被上锁的线程去解锁;通过递减AQS内部维护的state属性值,直到state减为0,表示资源已被解锁;当资源被解锁后,需要通过Unsafe的unpark方法,去唤醒CLH队列中,被挂起的第一个节点上的线程。
在2.2中说过,当我们使用无参构造器创建一把“锁”的时候,默认是使用NonfairSync这个内部类,也就是非公平锁;但是在源码中发现ReentrantLock还存在一个有参构造器,参数是一个boolean类型;
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
很明显,这种方式就是将选择权交给开发人员,当我们传入true时,就会创建一把“公平锁”。还是一样,先来看下公平锁的内部;
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
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;
}
}
从源码的角度,咱来看下,为什么一个叫“非公平锁”,另一个叫“公平锁”?
其实不难发现,NonfairSync内部的lock方法,它是一上来就通过cas机制去抢占state公共资源,抢不到才去执行acquire方法实现后续入队列等一系列的操作;而这里FairSync的lock方法,它是直接执行acquire方法,执行后续的操作。等于非公平锁,会去多争取一次资源,对于在CLH队列中等待的线程,是“不公平”的。
除了lock方法存在差异之外,在tryAcquire方法中,也存在着不同。FairSync类中,会多执行hasQueuedPredecessors方法,它是AQS下的一个公用方法,下面具体看下这个方法;
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
只有简短的几行,却有很多种可能性,但是整个方法主要功能就是判断当前线程是否需要入队列:返回false,队列为空,不对等待;返回true,队列不是空,去排队等待。下面需要重点讲下这一行代码:return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
返回false,情况也有两种:1、h != t** **是false,2、h != t是true,并且 (s = h.next) == null 是false, s.thread != Thread.currentThread()是false。
第一种情况比较简单,意思是头结点和尾节点是同一个,说明队列是空的,不需要排队等待,所以直接返回false。
第二种情况,头尾不是同一个节点,头部节点的下个节点也不是空,并且头部节点的下一个节点就是当前线程。其实就可以理解为,前面的资源刚释放,正好轮到当前线程来抢占资源,这种情况相对较少。
返回true,有两种情况:
h != t是true,并且 (s = h.next) == null 是true。
h != t是true,并且 (s = h.next) == null 是false, s.thread不等于Thread.currentThread()是true。
1、这里的头尾不是同一个节点是必要满足的条件,保证了队列起码不是空的。然后(s = h.next) == null 满足是true,这里解释起来就必须回顾下enq初始化队列这个方法。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
分享题目:从ReentrantLock角度解析AQS
浏览路径:http://www.csdahua.cn/qtweb/news48/92148.html网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网