本来这篇想写并发容器的,但是我看了下api文档,没啥深度,没啥可写的,照着api文档的描述敲一遍代码也差不多就懂了。所以也就决定这一片记一下这两天学习的AQS原理。
大家不要一看到原理两字,心里就有点退堂鼓,以为很难的感觉。难道那不是人写的代码吗,他能写出来,你就写不出来吗,也就是一个人的逻辑,明白了他的逻辑不也就懂了代码原理了嘛。
初入博客,有许多描述不清楚、语句不通顺的地方。请提出指正!!!
开始
synchronized锁的实现,它是jvm关键字,直接转换成字节码命令。 ReentrantLock是一个普通的代码类,是java代码通过逻辑来实现锁的效果,也正因为是java代码实现的,所以在灵活度上要比sync锁强。 在讲解是,我会以非公平锁示例来描述他发生的过程,理解了非公平锁,公平锁也自然就理解了。我将以不同的情况来描述Rreentrantlock当时要做的操作。
1. 初始状态下,第一个线程访问:
public static void main(String[] args) {
Lock rtLock =new ReentrantLock();
try {
rtLock.lock();
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rtLock.unlock();
}
}
断点运行这段代码,此时的状态:
可以看到rtLock中有4个字段:state、head、tail、exclusiveOwnerThread,
state表示的是锁的状态:0表示没有锁,0<state,则表示有锁或锁重入。
head和tail:表示的队列中的节点,Rtlock之所以能实现公平锁,就是因为队列的设计,下面的情节会说。
exclusiveOwnerThread:当前持有锁的线程.
这段是获取锁的源码,通过CAS算法去获取锁,我们这一小节是初始状态的前提下,所以CAS返回值肯定为true,然后设置当前持有锁的线程为当前线程。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2. RtLock的重入锁 继第一步,我们在main方法中,稍加改动
看到了state变成了2,看源码是怎么操作的:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
还是这段源码,这次compareAndSetState的CAS返回结果肯定为false,所以执行else中的方法,跟进去:
public final void acquire(int arg) {
//tryAcquire获取锁,若tryAcquire获取锁失败,则进入
//acquireQueued方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//先跟进去tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//当前线程
int c = getState();//此时锁的状态
//锁重入,则c肯定不为0,执行else if逻辑
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/**
*我们在上面说明了,有个表示当前持有锁的线程的字段,
getExclusiveOwnerThread为得到当前持有锁的线程,
*判断当前线程与此时持有锁的线程比较是否相等,相等则表示为
同一个线程,则state加1,返回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;
}
3. 此时处于上锁状态时,又新来一线程获取锁
waitStatus在AQS中有5个状态:
/** waitStatus value to indicate successor's thread needs unparking */
翻译:waitStatus value to 指示后续线程需要断开连接
static final int SIGNAL = -1;
/** waitStatus value to indicate thread has cancelled */
翻译:waitStatus value to 指示线程已取消
static final int CANCELLED = 1;
/** waitStatus value to indicate thread is waiting on condition */
翻译:waitStatus value to 指示线程正在等待条件
static final int CONDITION = -2;
/** waitStatus value to indicate the next acquireShared should
* unconditionally propagate*/
翻译:waitStatus value to 指示下一个acquireShared应无条件传播
static final int PROPAGATE = -3;
为0时,则以上都不是。
- 我们看到head(队列头部)的thread为空、waitstatus为-1以及next指向了下一个节点,thread为空则说明head是AQS创建的一个空节点,只不过,为啥要这么做呢?—程序之所以创建一个空的head节点,就是为了能准确的确定当前节点是否为有效队列中的第一个节点。
- tail(队列尾部)的waitstatus为0,上一个节点为head,下一个节点为空,thread为新来的线程。
记住这张图的信息,然后我们开始看源码:
继第2步,tryAcquire获取锁方法肯定返回false,然后“!false”为true,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
嵌套了一个方法addWaiter,我们进去:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//创建一个独占锁的node节点,Node.EXCLUSIVE表示独占锁
//队列中的尾部节点,由于这个线程是新来的,队列还未创建,所以tail现在肯定为空。
Node pred = tail;
//pred为空,则跳过
if (pred != null) {
node.prev = pred;//原来队列中的尾部节点,现在变成新节点的上一个节点
if (compareAndSetTail(pred, node)) {//通过cas算法,加入队列中
pred.next = node;//原来尾部节点的下一个节点为当前节点
return node;
}
}
enq(node);//加入队列,进入enq
return node;
}
private Node enq(final Node node) {
for (;;) {//相当于while(true),最好的写法是for(;;)
//第一次循环,tail为空,则执行if方法体,第2次循环不为空,
//则执行else方法体,
//这里说的第一次循环tail为空,是基于这个新来的线程是第二
//个竞争锁的线程,
//如果第3个线程来了,第一个线程还未释放锁,那这个tail肯定
//不为空。所以不要混淆
Node t = tail;
if (t == null) {
//新建一个节点,通过cas算法放入head中。
if (compareAndSetHead(new Node()))
//将头部又赋值给尾部,说明该队列是个循环列表,然
//后进行第2次循环
tail = head;
} else {
//这一步就是给tail重新赋值,将新线程的信息赋值给tail,然
//后循环结束
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
结束,我们看到addWaiter方法就是做了队列的创建,以及将新来的线程加入到创建的队列中,一个特点就是head节点是空的,有效的线程从队列中的第2个元素开始。然后,我们看acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//现在队列中虽然有两个节点,但是现在“真正有效”的节点
//只有tail节点,则p为head节点
final Node p = node.predecessor();
//现在第一个线程还未释放锁,则p==head为true,
//tryAcquire(arg)为false,跳过第一个if,执行第二个if
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;
//刚才看到了图片上head的waitStatus为-1,所以返回true
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 final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
看到源码,我们得到的信息是,去当前节点,上一个节点的waitStatus枚举值,由枚举值决定返回值,然后,parkAndCheckInterrupt方法,是直接将当前线程中断,那么这个if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())干的就是:由当前节点的上一个节点的状态来决定当前节点是否需要被中断。在这里我想说明一下:我看到了很多博客中说for(;;)是在自旋。确实是自旋,但是与jvm中对锁进行优化时进行的自旋锁有锁不同,jvm中的自旋锁,是为了防止阻塞导致的性能下降而引入的,而这里描述的自旋,仅仅只是自旋,并没有关于性能的优化设计,在这里,如果需要阻塞,就会阻塞。我也因为当时理解错误,找了好久的资料。
对于CLH队列,只是记录了线程的id,在需要唤醒的时候,根据这个id唤醒它,否则根据这个id找到这个线程后让他中断。
4. 看到了AQS的队列操作,再来看看解锁吧
解锁很简单,我不会在这里再那么多话,请自行分析代码
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) {
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;
}
//唤醒head节点下一个节点
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);
}
这是解锁,没啥好说的,可以说一下的就是在解锁之后,程序又去唤醒了head节点的下一个节点。节点在获取锁之后,将当前节点移出队列,就是if (p == head && tryAcquire(arg))中的逻辑。
**在这里,被唤醒的节点,从被中断的位置开始执行,也就是再次开始执行循环for(;;)体,则if (p == head && tryAcquire(arg))有可能成功(因为是非公平锁,所以有可能成功),通过自旋。**这里的自旋与sync锁的优化自旋不同,sync的自旋的设计是不想线程阻塞耗费性能,这里的自旋只是单纯的循环获取锁而已。
总结:当锁已经被占用的时候,非公平锁,会首先去尝试获取锁,如果获取锁失败,则乖乖的去排队。CLH队列,遵循FIFO规则,也就是,只要你入队了,那么你只能被按照公平锁的方式执行。而公平锁与非公平锁在代码上的不同就是在获取锁的时候,先去查看队列中是否有node节点,如果有,那就乖乖排队去,没有就直接获取锁。
另外,aqs又可以实现条件队列来操作对应的线程,详见博客
这只是reentrantlock中的很小的一部分,但是基本的流程逻辑也差不多就这样了,其他的方法多是对一些逻辑方法的强化等。
如果有时间真的可以好好读一读。
CLH队列:是Craig,Landin and Hagersten三个人发明的,所以叫CLH队列