本人由于考研所以很久没有系统的看过java。现在准备重新系统的将java(jdk12)并发包 java.util.concurrent 源码重新简单梳理一遍,借用给记录下来,方便日后的复习。此篇文章部分知识点来源于https://www.javadoop.com/。文章如有错误请各位大佬指出,共同进步共同学习!
分析并发包首先要了解AbstractQueuedSynchronizer(AQS),因为AQS是并发包的基础工具类。本文从ReentrantLock的公平锁出发,分析AbstractQueuedSynchronizer的工作过程。
lock与unlock的使用
public class Test implements Runnable {
//使用staic确保每个线程拿到的是同一把锁。
private static ReentrantLock lock=new ReentrantLock(true);
private static int i=0;
@Override
public void run() {
//加锁
lock.lock();
try {
for (int j=0;j<100000;j++) {
i++;
}
}finally {
//释放锁
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread a=new Thread(new Test());
Thread b=new Thread(new Test());
a.start();
b.start();
a.join();
b.join();
System.out.println(i);
}
两个线程同时对一个变量i进行加一操作。lock.lock()确保同一时间只有一个线程进行。输出结果为200000。
AQS结构
分析源码首先要分析其结构,下面为AQS抽象类中的属性
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//头节点 可以理解为当前持有锁的线程
private transient volatile Node head;
//尾节点 每个新的线程没有抢到锁都插入到最后,形成一个链表
private transient volatile Node tail;
//代表当前锁的状态,0代表没有被占用,大于0代表存在线程持有当前锁,这个值可以大于1,因为锁可重入,每次重入都加1.
private volatile int state;
//代表当前持有独占锁的线程。继承自AbstractOwnableSynchronizer。
private transient Thread exclusiveOwnerThread;
}
所以AQS的阻塞队列如下图所示
其实就是一个FIFO队列,公平锁就是每次抢占锁时需要判断队列中是否有其他比我更早等待的,谁先在队列中谁先拿到锁。
阻塞队列中每个线程被实例成一个节点,每个节点属性如下
static final class Node {
//标识当前节点在共享模式下
static final Node SHARED = new Node();
//标识当前节点在独占模式下
static final Node EXCLUSIVE = null;
//表示当前节点的等待状态,取值为下面的1,-1,-2,-3。
volatile int waitStatus;
//此节点的线程取消等待
static final int CANCELLED = 1;
//其后继节点对应的线程准备被唤醒
static final int SIGNAL = -1;
//下面两个属性此文章不涉及所以这里就不细说了
// waitStatus value to indicate thread is waiting on condition.
static final int CONDITION = -2;
//waitStatus value to indicate the next acquireShared should unconditionally propagate.
static final int PROPAGATE = -3;
//前驱节点的引用
volatile Node prev;
//后继节点的引用
volatile Node next;
//当前节点代表的线程。
volatile Thread thread;
//链接到正在等待状态的下一个节点,或共享的特殊值。
Node nextWaiter;
}
上面为基础知识,需要在心里一直有这么个结构,后面会多次涉及。下面开始分析ReentrantLock的公平锁。
ReentrantLock在内部使用内部类Sync来管理锁,所以真正获取和释放锁由内部类Sync来控制
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
.......................
}
}
Sync由 NonfairSync(非公平锁)和 FairSync(公平锁)实现
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
线程获取锁(抢锁)
这里我们分析公平锁
//当我们进行抢锁lock()时,会调用sync的acquire方法。
public void lock() {
sync.acquire(1);
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//acquire()来自Sync的父类AQS,直接贴过来方便阅读
//如果tryAcquire(arg)返回true说明抢到了锁直接返回
//否则进行acquireQueued将线程插入到阻塞队列中。
public final void acquire(int arg) {
//首先尝试获取一下锁,如果获取成功那么我就不用去阻塞队列中去排队了
if (!tryAcquire(arg) &&
//没有获取到锁,将此线程放入阻塞队列并挂起。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
@ReservedStackAccess
//尝试直接获取锁
//返回true代表此线程成功获取到锁,有两种情况返回true:
//1,当前没有线程持有锁。
//2,可重入锁,正在持有锁的线程和此线程为同一线程。当然重入几次就要对应释放几次锁。
protected final boolean tryAcquire(int acquires) {
//current为当前线程
final Thread current = Thread.currentThread();
//获取state的值
int c = getState();
//state=0表示当前没有线程持有锁。
if (c == 0) {
//因为是公平锁,先来后到,所以要检查阻塞队列是否有其他线程在等待。
if (!hasQueuedPredecessors() &&
//如果没有其他线程在等待,使用CAS来设置state值,成功则说明此线程就可以获得到锁了
//不成功则说明在同一时刻有其他线程比我先抢到了锁
//CAS原理这里不做过多解释
compareAndSetState(0, acquires)) {
//进入这里说明我已经成功获得了锁,exclusiveOwnerThread = thread; 告诉其他人是我占有了这把锁。
setExclusiveOwnerThread(current);
return true;
}
}
//进入这个分支说明是重入了,当前线程和持有锁的线程为同一个,执行state+1操作。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//到这里返回false说明此线程没有抢到锁。
return false;
}
//如果tryAcquire返回false,那么执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法。
//首先执行addWaiter(Node.EXCLUSIVE)
//这个方法就是把线程包装成node并加入到阻塞队列中
private Node addWaiter(Node mode) {
//参数mode=Node.EXCLUSIVE,即独占模式。
//对Node进行初始化,将节点的线程设为当前线程,nextWaiter设为独占模式。
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
//阻塞队列队尾不为空时将此节点从队尾插入。
if (oldTail != null) {
//node的前驱--->oldtail
node.setPrevRelaxed(oldTail);
//使用CAS将node设为队尾,即tail==node。使用CAS的原因是同时可能有其他线程在竞争入队。
//可以看出这是一个for(;;),所以一直竞争入队直到入队为止。
if (compareAndSetTail(oldTail, node)) {
//到这一步就是把新的node与之前的队尾进行双向连接。
oldTail.next = node;
//把新加入到队尾的节点返回。
return node;
}
} else {
//进入到这里表示阻塞队列为空,需要对阻塞队列进行初始化。
initializeSyncQueue();
//下面就是initializeSyncQueue的实现方法,其实就是初始化阻塞队列,head==tail
//private final void initializeSyncQueue() {
// Node h;
// if (HEAD.compareAndSet(this, null, (h = new Node())))
// tail = h;
// }
}
}
}
//现在又回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法。
//参数node为addWaiter(Node.EXCLUSIVE)返回的已经进入阻塞队列的node
//如果此方法返回true,那么就selfInterrupt(),所以正常情况应该返回false
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
//p为node节点的前驱节点。
final Node p = node.predecessor();
//如果p==head说明node为队列第一个阻塞的节点,所以进行尝试一次抢锁操作
//tryAcquire(arg)前面有提到过,忘了的往前翻翻。
if (p == head && tryAcquire(arg)) {
//说明抢到锁,那么将当前节点设为头节点。并将其前驱节点移除。
setHead(node);
p.next = null; // help GC
return interrupted;
}
//进入这个条件里说明当前node不是队头或者抢锁没有抢赢别人。
//执行shouldParkAfterFailedAcquire(p, node),作用是没有抢赢别人,是否需要挂起此线程。
if (shouldParkAfterFailedAcquire(p, node))
//parkAndCheckInterrupt就是挂起当前线程。
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//ws为前驱节点的waitStatus
int ws = pred.waitStatus;
//waitStatus==-1表示前驱节点正常,当前线程可以挂起,返回true。
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
//waitStatus>0说明前驱节点取消了排队,因为唤醒操作是由其前驱节点唤醒的,
//所以需要重新找到一个节点waitStatus<0的作为自己的前驱节点。
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
// 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
// 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
//返回false,因为前面是for循环,所以再次进来此方法,从第一个分支返回true。
return false;
}
//此方法就是把当前线程挂起,这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
}
说到这里,加锁操作基本就已经完事了,跟着思路自己再重新过一遍这个加锁操作。
解锁操作
如果上面看懂,那么解锁操作很简单。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//通过头节点唤醒线程
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//用c来判断是否已经完全释放锁,因为可能存在可重入锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c==0表示已经完全释放锁。不为0那么存在重入,还不能释放锁。
if (c == 0) {
free = true;
//把持有锁的线程置为null。
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//唤醒后继节点,但有可能后继节点不存在(s==null)或不符合唤醒条件(waitStatus>0),
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//从队尾往前找,找到排在最前面的符合唤醒条件的节点(waitStatus<=0)
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//唤醒线程
LockSupport.unpark(s.thread);
}
线程唤醒以后,回到前面方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //线程从这里被挂起。唤醒后继续向下运行
return Thread.interrupted();
}
//回到acquireQueued(final Node node, int arg)方法。这时node的前驱就是head了。
这里的加锁和释放锁的操作就结束了。