synchronized关键字的扩展:重入锁(ReentrantLock):
public class ReentrantLockDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run(){
for(int j=0;j<10000;i++){
lock.lock();
try{
i++;
}finally{
lock.unlock();
}
}
}
}
与关键字synchronized相比,重入锁有着明显的灵活性,何时加锁,何时放锁,重入锁之所以叫重入锁,是因为这种锁可以反复进入。当然,这种反复仅仅局限于一个线程。比如可以这样写:
lock.lock();
lock.lock();
try{
i++;
}finally{
lock.unlock();
lock.unlock();
}
一个线程连续两次获得同一把锁是允许的。
重入锁的中断响应:
对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,这个线程还可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。这个对处理死锁是有一定帮助的。
下面我们来从源码正式分析ReentrantLock的原理:
我们一路点进lock()方法:我们会发现abstract void lock();
我们看看实现它的类
这个就是熟悉的公平锁与非公平锁,我们先来看看公平锁
一路点到:acquire这个方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
点进tryAcquire,这个方法的意思是:尝试加锁
@ReservedStackAccess
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;
}
}
/**
* 这里体现重入,当current这个值等于getExclusiveOwnerThread()方法的返回值(这个方法是获取并返回当前持有锁的线程)
* 就给状态值加1
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
哪里公平呢?
ReentranLock在加锁时执行到上面的tryAcquire方法,比如现在有一个线程被要lock(),会先定义一个变量获取一个状态值,这个状态就是现在这个锁是自由的还是被持有的,显然第一个线程进来锁一定是自由的等于0,判断c==0一定是成立的,于是向下执行,请看这个函数 !hasQueuedPredecessors()。当第一个线程进来的时候,会先判断这个函数,这个函数的意义是判断当前队列中,有没有早早已经来等锁的线程,换句话说有没有必要将当前正在执行的线程加入队列,如果没有,那么返回false,再取非,于是!hasQueuedPredecessors()返回true,所有才有机会执行CAS改变状态获取锁,这时候假设我们有第二个线程来竞争,线程一还没有释放锁,所以状态码是1,第二个线程判断c!=0,会直接返回false,然后把它加入同步队列进行等待。
所以公平就公平在!hasQueuedPredecessors()这个方法会进行判断从而去调度线程,这里面涉及到一个著名的数据结构,AQS队列,这个队列就是把阻塞的线程都存在里面,唤醒的时候,先加进来的线程先唤醒,这就是公平,我们来看看非公平锁。
下面是非公平锁:非公平锁是的加锁函数是nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//这里少了!hasQueuedPredecessors()
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;
}
看到没有!!!!!与公平锁相比,就少了!hasQueuedPredecessors()这个函数
我们看到了比公平锁少了!hasQueuedPredecessors()这个函数,那么就意味着,一个线程拿锁时,不会知道有没有比他最先来的但是已经在等待的线程,也就是说当几个线程竞争时,假定有两个线程已经被加入到了同步队列,这时候一个线程进来抢走了锁,那么前两个在队列中的线程,没错!白!等!了!,但是那个线程一来就把锁抢走了,这就体现了不公平。
在说这个很重要的方法!hasQueuedPredecessors()之前我们需要先搞明白,一个被阻塞的线程是怎么成为一个Node入队的,我们以公平锁为例。这里我们需要一个Node类,这里只列举一些重要的属性:
class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
private transient volatile Node head;
private transient volatile Node tail;
volatile int waitStatus;
}
假设有两个线程t1,t2来了,且存在竞争,当t1先拿到锁的执行权,很轻松加锁成功并且把锁的状态改为1,没释放锁的时候,t2来了,判断锁的状态为1,这时候条件不成立,所以它会返回false,也就是说tryAcquire方法会返回false。这时候我们再放上acquire这个方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们可以看到 !tryAcquire(arg)就会返回true因为tryAcquire返回了false,所以第一个条件成立,于是开始先调用addWaiter方法。
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;
}
这个方法就是入队操作,tail在这时候等于null,所以会直接执行enq方法,注意这时候node就是当前正在执行的t2线程。
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;
}
}
}
}
在enq方法中,这是一个死循环,tail一定等于null因为第一次进来,所以第一个条件成立,这里有一个非常重要的点,compareAndSetHead(new Node()),这里代表使用CAS算法设置head指向一个new Node(),所以new出来的这个Node对象的Thread一定等于null,因为没有对它进行设置,这就意味着,不论什么情况只要你有需要加入队列,那么队头一定会设置一个Thread=null的结点维护这个队列,所以这个队列的head就指向了一个Thread=null的Node结点,然后tail=head,这就让tail也指向这个结点所以就是下面这样:
因为是死循环,所以第二遍循环tail不再为空,已经指向了一个节点,所以会执行else,else才是真正把t2这个线程维护好放入结点,就是下面这样:
好的,至此一个没有抢到锁的线程就这样被加入队列,但是我们从代码发现,队列是加入了,但是线程似乎没有要停的意思,就是说我们还没有看到哪句代码让线程停下,ReentrantLock用的是自旋锁,我们看看在哪里体现的,addWaiter方法结束之后,acquireQueued会继续执行。
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);
}
}
同样是一个死循环,node一样代表t2这个线程的Node结点,这个predecessor()函数拿到node的上一个节点,我们通过上面的图发现,node.predecessor()就是那个Thread=null的结点,p == head成立,这时候!!!!!!!!!!!!我们看到又tryAcquire(arg)了去尝试获取锁,为什么呢,因为如果你入队完成了,这时候t1也把锁释放了,那么你就不用再等待就直接去执行你的任务即可,这里我们假设如果tryAcquire(arg)成功了,那么它会将t2出队,然后去执行任务,第一个if里面的都是在t2出队后维护这个队列所以没什么,所以我们重点假设它还是没有tryAcquire(arg)成功,一定会执行第二个if,我们先来看看shouldParkAfterFailedAcquire。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
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) {
/*
* 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里要注意,函数的两个参数,p是代表当前节点的上一个节点,node才是表示当前节点。
pred.waitStatus初始是0,所以ws初始为0,注意这里的ws,是pred这个节点的waitStatus的值,pred是上一个节点的就是那个Thread为空的节点,Node.SIGNAL是-1,这些都是Node类中定义的数值,很显然,会直接执行else,这时候会用CAS将pred.waitStatus设置为-1然后返回false,这里返回false就会导致又会返回acquireQueued方法中去继续死循环执行,又回去tryAcquire(arg),假设还是获取不到,进入这个方法,那么这时候第一个if条件就符合了,会返回true,接着会执行 parkAndCheckInterrupt()。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
好的看到这里,park了,也就是阻塞了这个线程,让它不能正常返回,到这里,t2被真正阻塞。
但是我们也发现了,t2在这里判断并改变waitStatus数值的时候,是改变的那个Thread=null的结点的waitStatus,代码是这么写的compareAndSetWaitStatus(pred, ws, Node.SIGNAL),并不是自己t2的waitStatus,也就是说,到底阻不阻塞,执不执行parkAndCheckInterrupt方法,是当前执行的线程去改变上一个节点的值决定的,如果t3来了加入了队列,那么它就要去改变t2的waitStatus去让自己自旋。将t2的waitStatus值为0然后变到-1,循环了两次,也就是自旋了两次,为什么是两次,我也说不清,可能大师经过测试两次最合适吧,两次拿不到锁,就阻塞,这应该是最合适的。
好的我们遗留了一个问题,hasQueuedPredecessors()这个方法,这个方法对线程的执行起了关键性的作用。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法只有简简单单几行代码,但是难的一匹,不知道大家是否记得,这个函数执行的条件必须是c==0,c是锁的状态,也就是锁处于自由状态的时候,才会去执行这个方法。这个方法的作用,就是判断你的队列中有没有已经早早就来了等待的线程,如果有,那么我当前线程就加入队列等待,如果没有,那么我去获取锁。
假设有t1,t2,t3三个线程,t1进来,锁一定是自由的,t1拿到了锁,进入这个方法,h一定等于t等于null,因为这时候队列里面没有任何元素,所以返回false,一路返回给t1加锁,这时候t2进来,t1还没有释放,所以不执行这个方法,t2直接被加入队列等待,这时候t3来了,t1恰好释放了锁,t3发现c==0,进入这个方法,这时候h!=t成立因为t2已经加入,队首和队尾一定不相同,然后(s = h.next) == null,这个是不成立的h.next是t2,所以不为null,s.thread != Thread.currentThread()成立因为s.thread是t2,Thread.currentThread()是t3,所以这个方法返回true,tryAcquire返回false,于是t3就要被加入队列。唤醒的就是t2,不论什么情况,只要队列中有等待的线程,这个方法都会获取到,这个函数就是保证先来后到的原则。这时候t2进来,t1还没有释放,所以不执行这个方法,t2直接被加入队列等待。
然后我们来说说它如何解锁,我们来看看unlock方法,就是如何进行解锁的
我们直接看release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
同样它会先调用tryRelease(arg)方法尝试放弃锁。
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;
}
tryRelease会先使锁的状态-1,此时c=0,然后设置拥有锁的线程为null,然后返回true,返回到release方法,此时我们队列不为null,所以h!=null成立,h.waitStatus != 0也成立因为没有人修改这个值,所以unparkSuccessor(h)这里面就是将锁释放。这里释放锁就结束了。