转载自:http://www.it165.net/pro/html/201402/9171.html
ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。也就是说ReentrantLock在同一个时间点只能被一个线程获取。
Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。
ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。
在“公平锁”的机制下,线程依次排队获取锁;“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
来看一下ReentrantLock互斥锁的构造函数:
1.
public
ReentrantLock() {
// 默认非公平锁
2.
sync =
new
NonfairSync();
3.
}
4.
public
ReentrantLock(
boolean
fair) {
// 通过参数判断选择公开锁还是非公平锁
5.
sync = fair ?
new
FairSync() :
new
NonfairSync();
6.
}
当我们在程序中通过如下方式获取到实例后,就可以调用lock()方法进行独占锁的获取了,如下:
1.
private
Lock lock =
new
ReentrantLock();
调用lock.lock()方法可以获取独占锁,源代码如下:
1.
public
void
lock() {
2.
sync.lock();
3.
}
1、获取公平的独占锁
其基本的方法调用流程如下所示,后面将会进行详细的讲解。
01.
static
final
class
FairSync
extends
Sync {
02.
03.
final
void
lock() {
04.
acquire(
1
);
// 直接调用acquire()方法获取锁
05.
}
06.
// 试着去获取锁. 只有递归调用、或者没有等待者或者在等待队列的第一个时获取到该锁.
07.
protected
final
boolean
tryAcquire(
int
acquires) {
08.
final
Thread current = Thread.currentThread();
// 获取当前线程
09.
int
c = getState();
// 获取独占锁的状态
10.
if
(c ==
0
) {
// c=0 意味着锁没有被任何线程锁拥有
11.
// 若锁没有被任何线程锁拥有,则判断当前线程是不是CLH队列中的第一个线程线程,
12.
if
(!hasQueuedPredecessors() && compareAndSetState(
0
, acquires)) {
13.
setExclusiveOwnerThread(current);
// 获取该锁,设置锁的状态,并切设置锁的拥有者为当前线程
14.
return
true
;
15.
}
16.
}
else
if
(current == getExclusiveOwnerThread()) {
//独占锁的拥有者已经为当前线程
17.
int
nextc = c + acquires;
18.
if
(nextc <
0
)
19.
throw
new
Error(
"Maximum lock count exceeded"
);
20.
setState(nextc);
// 更新锁的状态
21.
return
true
;
22.
}
23.
return
false
;
24.
}
25.
}
在静态的不变类中调用lock()方法,这个方法会调用AbstractQueueSynchronizer类中的acquire()方法,并传递参数1. 由于ReentrantLock是可重入锁,所以独占锁可以被单个线程多此获取,每获取1次就将锁的状态加1。 也就是说,初次获取锁时,通过acquire(1)将锁的状态值设为1;再次获取锁时,将锁的状态值设为2;依次类推...
1.
public
final
void
acquire(
int
arg) {
2.
if
(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
3.
selfInterrupt();
4.
}
首先要看tryAcquire()方法调用返回的结果。如果锁不是被当前的线程占用,则返回false,否则直接就获取到这个锁了。
对比代码和方法调用流程图,下一部就是调用addWaiter()方法了,并传递参数Node.EXCLUSIVE,表示是独占锁类型。
01.
private
Node addWaiter(Node mode) {
02.
// 新建一个Node节点,节点对应的线程是当前线程,当前线程的锁的模型是mode。
03.
Node node =
new
Node(Thread.currentThread(), mode);
04.
Node pred = tail;
05.
// 若队列不为空,则将当前线程添加到队列末尾
06.
if
(pred !=
null
) {
07.
node.prev = pred;
08.
if
(compareAndSetTail(pred, node)) {
// 以原子的方式进行添加
09.
pred.next = node;
10.
return
node;
11.
}
12.
}
13.
enq(node);
// 若队列为空,则调用enq()新建队列,然后再将当前线程添加到队列中
14.
return
node;
15.
}
如上方法是将这个请求独占锁类型的线程添加到了队列的末尾。由于在添加的时候,要防止可能其他的线程对队列进行了修改,所以调用了compareAndSetTail()方法进行处理,如果对这个方法的基本原理不明白,可以参见:
传送门 - http://write.blog.csdn.net/postedit/18908493
接下来调用acquireQueued()方法,并将代表当前线程的节点做为参数传递,源码如下:
01.
final
boolean
acquireQueued(
final
Node node,
int
arg) {
02.
boolean
failed =
true
;
03.
try
{
04.
// interrupted表示在队列的调度中,当前线程在休眠时,有没有被中断过
05.
boolean
interrupted =
false
;
06.
for
(;;) {
07.
// 获取上一个节点,node是当前线程对应的节点,这里就意味着获取上一个等待锁的线程
08.
final
Node p = node.predecessor();
09.
if
(p == head && tryAcquire(arg)) {
// 使用p==head来保证锁的公平性。如果当前线程是因为“线程被中断”而唤醒,那么显然就不是公平了
10.
setHead(node);
11.
p.next =
null
;
// help GC
12.
failed =
false
;
13.
return
interrupted;
// 只有在这里才能跳出死循环
14.
}
15.
if
(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
16.
interrupted =
true
;
17.
}
18.
}
finally
{
19.
if
(failed)
20.
cancelAcquire(node);
21.
}
22.
}
注意,在for(;;)死循环中,唯一跳出去的途径就是满足p==head和tryAcquire()方法返回值为true。也就是说,当前线程会根据公平性原则进行阻塞等待直到获取锁为止。
(1)当p==head时,表示当前线程前面的线程已经得到执行,这样就保证了锁的公正性。即按照队列的先后顺序进行执行。
(2)tryAcquire()方法返回true时,表示当前线程成功获取了独占公平锁。可以进行返回了。但是返回的并不是true,而是interrupted变量的值。那么这又是怎么回事呢?
其实为了保证绝对的公平性,代码考虑到了中断这种特殊的情况。线程被阻塞后有在这样的代码中两种情况可以唤醒:
第1种情况:unpark()唤醒。“前继节点对应的线程”使用完锁之后,通过unpark()方式唤醒当前线程。
第2种情况:中断唤醒。其它线程通过interrupt()中断当前线程。
对于第2种情况,线程在阻塞状态被中断唤醒而获取到cpu执行权利。但是,如果该线程的前面还有其它等待锁的线程,根据公平性原则,该线程依然无法获取到锁。它会再次阻塞! 该线程再次阻塞,直到该线程被它的前面等待锁的线程锁唤醒;线程才会获取锁并执行。
所以如果以下代码如果执行的话,会让interrupted变量为true。
1.
if
(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
2.
interrupted =
true
;
01.
// 获取锁失败后判断当前线程是否应该阻塞
02.
private
static
boolean
shouldParkAfterFailedAcquire(Node pred, Node node) {
03.
int
ws = pred.waitStatus;
04.
// 如果前继节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true
05.
if
(ws == Node.SIGNAL)
06.
return
true
;
07.
if
(ws >
0
) {
// 前继节点是取消状态
08.
do
{
09.
node.prev = pred = pred.prev;
//设置 当前节点的 前继节点为 原前继节点的前继节点
10.
}
while
(pred.waitStatus >
0
);
11.
pred.next = node;
12.
}
else
{
13.
// 如果前继节点为0或者共享锁状态,则设置前继节点为SIGNAL状态
14.
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
15.
}
16.
return
false
;
17.
}
也就是说:
(1)如果前继节点状态为SIGNAL(后继线程需要被唤醒),表明当前节点需要被unpark(唤醒),此时则返回true。则interrupted值为false,当前线程不会产生自中断。
(2)如果前继节点状态为CANCELLED(ws>0),说明前继节点已经被取消,则通过先前回溯找到一个有效(非CANCELLED状态)的节点,并返回false
(3)如果前继节点状态为非SIGNAL、非CANCELLED,那么就是需要被Condition唤醒或者是共享的线程,则设置前继的状态为SIGNAL,并返回false。
1.
// Convenience method to park and then check if interrupted
2.
private
final
boolean
parkAndCheckInterrupt() {
3.
LockSupport.park(
this
);
// 通过LockSupport的park()阻塞当前线程
4.
return
Thread.interrupted();
// 返回线程的中断状态
5.
}
如果acquire()方法的if条件判断全部为真,则会产生一个自中断,如下:
1.
// 当前线程产生一个自我中断
2.
private
static
void
selfInterrupt() {
3.
Thread.currentThread().interrupt();
4.
}
1、获取非公平的独占锁
来看一下非公平独占锁调用方法的流程,如下图:
其实大部分的流程是一样的,只是在开始的时候实现的方法不一样,如下:
01.
// 非公平方式获取锁
02.
static
final
class
NonfairSync
extends
Sync {
03.
final
void
lock() {
04.
if
(compareAndSetState(
0
,
1
))
// 如果线程没有被持有,则以原子方式设置为1,表示持有
05.
setExclusiveOwnerThread(Thread.currentThread());
// 设置为当前线程持有
06.
else
07.
acquire(
1
);
// 锁被持有,调用acquire()方法获取
08.
}
09.
protected
final
boolean
tryAcquire(
int
acquires) {
10.
return
nonfairTryAcquire(acquires);
11.
}
12.
}
公平方式获取时是直接调用acquire()方法,而非公平锁获取首先会判断锁有没有被占用,如果没有则直接获取。接下来再调用acquire()方法,其实在调用acquire()方法时与获取公平锁的流程是一样的,这里就不再说了,看一下tryAcquire()方法中调用的nonfairTryAcquire()方法,如下:
01.
// 不公平方式来获取锁状态
02.
final
boolean
nonfairTryAcquire(
int
acquires) {
03.
final
Thread current = Thread.currentThread();
// 获取当前线程
04.
int
c = getState();
// 获取锁的状态
05.
if
(c ==
0
) {
// c=0意味着锁没有被任何线程锁拥有
06.
// 若锁没有被任何线程锁拥有,则通过CAS函数设置锁的状态为acquires
07.
if
(compareAndSetState(
0
, acquires)) {
08.
setExclusiveOwnerThread(current);
//设置当前线程为锁的持有者
09.
return
true
;
10.
}
11.
}
else
if
(current == getExclusiveOwnerThread()) {
12.
// 如果锁的持有者已经是当前线程,则将更新锁的状态。
13.
int
nextc = c + acquires;
14.
if
(nextc <
0
)
// overflow
15.
throw
new
Error(
"Maximum lock count exceeded"
);
16.
setState(nextc);
17.
return
true
;
18.
}
19.
return
false
;
20.
}
这里与获取公开锁也有区别。公正锁在锁没有被占有有时候,还会比较当前线程是不是在队列的头部,然后再决定,这里是直接获取到了这个独占锁,也体现出了锁的非公平获取。
3、锁的释放操作
释放操作的流程相对简单,首先调用unlock()方法,代码如下:
1.
public
void
unlock() {
2.
sync.release(
1
);
3.
}
由于锁是可重入的,所以对于同一个线程,每释放锁一次,锁的状态要减去1。
01.
// 试着释放当前线程持有的锁并唤醒后继节点
02.
public
final
boolean
release(
int
arg) {
03.
if
(tryRelease(arg)) {
//试着释放当前线程持有的锁
04.
Node h = head;
05.
if
(h !=
null
&& h.waitStatus !=
0
)
06.
unparkSuccessor(h);
// 唤醒后继节点
07.
return
true
;
08.
}
09.
return
false
;
10.
}
释放锁时,调用tryRelease()进行释放。
01.
protected
final
boolean
tryRelease(
int
releases) {
02.
int
c = getState() - releases;
// c是本次释放锁之后的状态
03.
// 如果当前线程不是锁的持有者,则抛出异常
04.
if
(Thread.currentThread() != getExclusiveOwnerThread())
05.
throw
new
IllegalMonitorStateException();
06.
boolean
free =
false
;
07.
// 如果锁已经被当前线程彻底释放
08.
if
(c ==
0
) {
09.
free =
true
;
10.
setExclusiveOwnerThread(
null
);
//设置锁的持有者为null,即锁是可获取状态
11.
}
12.
setState(c);
// 设置当前线程的锁的状态。
13.
return
free;
14.
}
更新当前线程对应的锁的状态。如果当前线程对锁已经彻底释放,也就是state为0,则设置锁的持有线程为null,设置当前线程的状态为空,然后唤醒后继线程。
01.
// 唤醒一个有效的后继节点
02.
private
void
unparkSuccessor(Node node) {
03.
int
ws = node.waitStatus;
// 获取当前线程的状态
04.
if
(ws <
0
)
// 如果状态<0,以原子的方式设置状态=0
05.
compareAndSetWaitStatus(node, ws,
0
);
06.
// 获取当前节点的有效的后继节点,无效的话,则通过for循环进行获取。
07.
// 这里的有效,是指“后继节点对应的线程状态<=0
08.
Node s = node.next;
09.
if
(s ==
null
|| s.waitStatus >
0
) {
// 为空或已经被取消
10.
s =
null
;
11.
for
(Node t = tail; t !=
null
&& t != node; t = t.prev)
// 队列从后到前查找waitStatus<=0的节点
12.
if
(t.waitStatus <=
0
)
13.
s = t;
14.
}
15.
if
(s !=
null
)
// 唤醒后继节点s对应的线程
16.
LockSupport.unpark(s.thread);
17.
}
需要注意的是,在使用这个锁的时候,必须在finally块中释放锁。否则,如果在被保护的代码块中抛出异常,那么这个锁就永远也无法得到释放。