一、排它锁,实现有公平锁和非公平锁两种。区别:公平锁吞吐量更小,能保持更高的有序性和减少饥饿性,但是不能保证完整的公平性。
二、推荐使用方式:
*
<pre>
{
@code
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
* }}
</pre>
|
三、lock
1.FairSync.lock:
final void lock() {
acquire(
1);
}
|
NoFairSync.lock:
final void lock() {
if (compareAndSetState(
0,
1))
setExclusiveOwnerThread(Thread.
currentThread());
else
acquire(
1);
}
|
2.acquire:acquire方法封装在AQS中:
AQS.acquire(int arg):
public final void acquire(
int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.
EXCLUSIVE), arg))
selfInterrupt();
}
|
解读:尝试tryAcquire(arg),如果成功,则表示获取当前线程获取所成功,如果失败则将当前线程放入CLH等待队列中。
注意:
所以,不同的实现类有不同的tryAcquire(arg)方法,这是AQS给继承类留的其中一个窗口。
|
关于selfInterrupt:具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
interrupt和interrupted配合使用。
|
3.tryAcquire(int)在FairSync和NoFairSync中都有各自的实现。
先看FairSync.tryAcquire(int)的实现方法:
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; } |
1.new ReentrantLock()得到的对象,初始化state=0
|
2..hasQueuedPredecessors():
如果队列中有某thread排在当前thread前面,则返回true;
如果队列为空或者队列只有当前thread,则返回false。
|
3.
if(!
this.hasQueuedPredecessors() &&
this.compareAndSetState(
0, var1)) {
this.setExclusiveOwnerThread(var2);
return true;
}
如果没有thread排在当前thread前面,且当前threadCAS成功,则使
exclusiveOwnerThread
= 当前线程,并返回true。
注意:compareAndSetState是原子性的,所以如果多个线程同时调用此方法,只有一个线程成功,也就只有一个线程能够执行
this
.setExclusiveOwnerThread(var2);
return true
;
不知道是不是这个原因,
Doug Lea并没有对
setExclusiveOwnerThread 方法加volatile+CAS或者synchronized限制。
其实注意到后面的else if中的判断条件,也只有一个线程能够返回true,所以对于后续的方法并没有加入原子性限定,因为执行此方法的
|
|
再看NoFairSync.tryAcquire(int):
protected final boolean
tryAcquire(
int acquires) {
return nonfairTryAcquire(acquires);
}
|
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; } |
这个处理方式大致跟FairSync.tryAcquire差不多,主要不同的是,noFair不需要管等待队列CLH中是否有当前线程的祖先(processor),这也是noFair的原因。
|
4.如果tryAcquire(int)失败,则执行后续操作,先看addWaiter方法:
在解析addWaiter之前先了解Node:
对Node介绍比较详细的参考:
|
另外,补充非常重要的一点,也是我之前的困惑:对于CLH队列和Conditions队列的区别,只知道他们是不同的队列,但是具体的细节没有摸清楚。现在有一段能很好解答我疑问的源码注解:
*
<p>
Threads waiting on Conditions use the same nodes, but
* use an additional link. Conditions only need to link nodes
* in simple (non-concurrent) linked queues because they are
* only accessed when exclusively held. Upon await, a node is
* inserted into a condition queue. Upon signal, the node is
* transferred to the main queue. A special value of status
* field is used to mark which queue a node is on.
1.CLH和Conditions队列的节点单元都是Node。
2.CLH是双向,Conditions是单向队列。
3.只有在exclusive情况下才会用到Conditions队列。
4.CLH队列中的Node在某种情况下,将某个或某些Node移到Conditions队列中,在某种情况下被移回CLH。
5.Conditions算是CLH等待队列的过度队列。
|
AQS.addWaiter(Node mode):
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure 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; } } } } |
真正有意思的底层代码!!!但这两部分还是挺简单的。
步骤:
1.拿出tail(prev=tail),判断tail是否为空,不为空,则node.prev = tail;如果为空,则执行步骤3
2.尝试将node置为tail,如果置换成功,则prev.next=node,这样就成功在尾部加入node,且使node成为新的tail;如果置换失败,则执行步骤3
3.enq(node),先判断tail(prev=tail)是否为null,如果是,说明CLH队列还未创建,先创建CLH队列,且使head=tail=new Node,(这个new Node也有说法,他其实是一个空thread,说明head是一个无意义节点。);如果不为空,则执行步骤2。整个过程放在无线循环for里,也很容易想到原因:多个线程同时执行addwaiter,那也得一个一个放入tail中,毕竟CAS(tail,node)是原子性操作。
|
5.我们先要搞清楚,addWaiter到底干了什么?它其实是在当前thread在第一次尝试获取锁之后,不得不进入等待队列的操作。
这就是addWaiter,它让当前thread进入等待队列。但仅此而已,因为addWaiter并没有让当前thread进入阻塞状态(park),以及在
无限循环中伺机获取锁,并继续执行(unpark)。而这,是
acquireQueued(
final
Node node,
int
arg) 要干的事。
但在介绍
acquireQueued(
final
Node node,
int
arg) 之前,我需要搞清楚这个
waitStatus到底是什么?其实在之前看过很多
类似状态位,各种状态,而waitStatus目的很强,但要真正理解这几个状态位的意思,或者说是用途,还是要结合
acquireQueued(
final
Node node,
int
arg)
来看,尤其是SIGNAL,为什么偏偏是要在遇到前继节点(是泛指,只要排在当前节点前面,都是前继节点)是SIGNAL时,就能把当前线程状态操作成阻塞呢?其实很简单,设计者在想到
SIGNAL的用途就是这样:前继节点是SIGNAL状态了,后继节点就能进入park了。
SIGNiAL就像是一个接力棒,Node是认SIGNAL的,
就好比人,只要排在拥有接力棒的人后面,总能轮到自己拿到这个接力棒的。(我是不是很有才!!!)
CANCELLED[
1
]
-- 当前线程已被取消
SIGNAL
[-
1
]
-- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
CONDITION[-
2
]
-- 当前线程(处在Condition休眠状态)在等待Condition唤醒
PROPAGATE[-
3
]
-- (共享锁)其它线程获取到“共享锁”
[
0
]
-- 当前线程不属于上面的任何一种状态。初始状态
|
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); } } |
1.首先这是在无限循环中执行的
2.如果当前线程在论资排辈终于排到它了,很兴奋,马上tryAcquire获取锁,如果成功,改写head,自己成为head,将前head注销,以便GC。并且自己处于head的时候,node.thread(即head.thread )=null。仔细想想看,Node的作用就是让排在CLH对了的thread获取锁,而此时既然已经获得到了锁,此Node的目的就达到了,那么node.thread就没有存在的必要了(占内存,兄弟)。另外,获取到锁之后,就能让当前线程interrupt了,前面已经介绍了interrupt的作用。
3.
shouldParkAfterFailedAcquire:在没轮到自己之前,找一个SIGNAL状态的前继,接在它后面。遇到CANCELLED的Node就移除它,遇到waitStatus=0或PROPAGATE的,就将其状态改为SIGNAL,这样就能最终实现返回true,个人认为返回false的可能性为0,置于后面要return false,完全是因为代码层面的返回需要。
4.
parkAndCheckInterrupt:一旦寻找到有或者自我创造带有SIGNAL的前继Node,就能安心park了。
if (
shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted =
true;
这段代码会一直执行,知道当前线程排到队列的头部了。
5.parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.
park(
this);
return Thread.
interrupted();
}
结合:
if (
shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted =
true;
再结合知识点:
调用线程的interrupt方法,并不能真正中断线程,只是给线程做了中断状态的标志Thread.interrupted():测试当前线程是否处于中断状态。执行后将中断状态标志为false
Thread.isInterrupte(): 测试线程Thread对象是否已经处于中断状态。但不具有清除功能
发现:如果在parkAndCheckInterrupt 中返回的是true,说明此thread是interrupted状态,但是
Thread.interrupted() 会改变thread的状态,所以需要重新将其变为interrupted,所以才有了
public final void acquire(
int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.
EXCLUSIVE), arg))
selfInterrupt();
}
的selfInterrupt 。
6.如果失败获取锁,这里失败的原因,查看代码,只有在
protected boolean
tryAcquire(
int arg) {
throw new UnsupportedOperationException();
}
情况会发生。
执行cancelAcquire操作:
看源码会执行一些清除cancelled状态的Node,并
在某种情况下,通知后继node继续执行unparkSuccessor(node)
|
总结:要实现AQS,比如ReentrantLock,步骤:
1.定义一个实现类,去继承AQS,并实现.lock方法,此方法无论怎么变行,会调用AQS的acquire(int)方法
2.在实现类中去定义tryAcquire(int)方法,这会决定是exclusive mode还是share mode
3.将此实现类作为外层类(如ReentrantLock)的一个属性
四、unlock
unlock相对简单,直接AQS.release(int):
public final boolean release(
int arg) {
if (tryRelease(arg)) {
Node h =
head;
if (h !=
null && h.
waitStatus !=
0)
unparkSuccessor(h);
return true;
}
return false;
}
|
一开始我有疑问:为什么unparkSuccessor不是从当前线程开始去找它的后继者。但是知道撸过lock之后就知道,原因有两:
1.在lock过程中,当前thread成功获取锁之后,会将Node.thread=null
2.在获取锁后,当前node是会被设置为head节点,所以这里直接从head开始,没毛病。
|
要学习Condition queue,其实LinkedBlockingQueue是一个非常好的学习例子。
还有其它重要方法,未完待续。。。