首先简单介绍一个ReentrantLock这个锁。
ReentrantLock是java中可重入锁的一个实现,一次只能有一个线程持有锁,即就是所谓的独占锁。ReentrantLock包含三个内部类:Sync、NonfairSync、FairSync,通过构造函数的参数参数fair决定,true为公平锁实现,false为非公平锁实现。默认是非公平锁。
(为什么默认是非公平锁?因为非公平锁的执行效率高于公平锁)
公平锁:是指在并发环境下,每个线程在获取锁的时候都会先查看此锁维护的等候队列,如果为空,或者当前线程是等待队列中的 第一个,就占有锁,否则就加入到等候队列中去,按照先进先出的原则从队列中取得自己。
非公平锁:直接尝试获取锁,如果获取失败,则尝试使用公平性锁的那种方式。
reentrantLock结构图
非公平锁(NonFairSync)
lock()加锁方法
final void lock() {
if (compareAndSetState(0, 1))
//当线程进来之后,首先判断当前状态的值,如果值为0,通过CAS操作,设置状态值为1,那么竞争锁成功。
setExclusiveOwnerThread(Thread.currentThread());
else//如果上面设置失败的话,直接调用acquire()方法。
acquire(1);
}
在这里需要简单说一下这个当前状态(state),state是AQS中的一个结构变量值,即同步状态变量值。源码定义如下所示:
【private volatile int state;】这是一个带volatile前缀的int值,是一个类似计数器的东西,。在不同的同步组件中具有不同的含义,在ReentrantLock这个类中,state用来表示该锁被线程重入的次数。当state为0的时候,表示该锁不被任何线程所持有;当state为1的时候,表示线程恰好持有该锁一次;当state大于1的时候,表示该锁被线程重入state次。因为这是一个会被并发访问的量,所以用volatile修饰,保证其可见性。
private transient Thread exclusiveOwnerThread;
这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。
再次解释一下lock()方法:首先尝试快速获取锁,以CAS的方式将state值更新为1,且只有当state的值为0的时候才能够更新成功,因为state值在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时,该操作才能够成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就算结束了。若获取失败的话,则执行acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法的主要逻辑实现在if判断条件中,这里面有3个方法:tryAcquire()、acquireQueued()、selfInterrupt(),这三个方法分别封装了加锁流程中的主要处理逻辑,接下来一一进行介绍。
尝试获取锁的通用方法:tryAcquire()
tryAcquire()是AQS中定义的钩子方法,如下所示:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法默认抛出异常,ReentrantLock在公平锁和非公平锁模式下对此方法具有不同的实现,非公平模式的实现如下:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
底层调用nonfairTryAcquire()方法,从方法名就可以很明显看出是在非公平的语境下。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();获取当前线程的实例。
int c = getState();获取state变量的值,即该锁被线程重入的次数。
if (c == 0) {state为0,表示当前锁未被任何线程所持有
if (compareAndSetState(0, acquires)) {以CAS的方式获取锁
setExclusiveOwnerThread(current);将当前线程标记为持有该锁的线程
return true;获取锁成功,非重入
}
}
else if (current == getExclusiveOwnerThread()) {当前线程就是获取该锁的线程,说明该锁被重入了
int nextc = c + acquires;计算state变量要更新的值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);非同步方式更新state的值
return true;获取锁成功,重入
}
return false;走到这一步,说明获取锁失败了!
}
这是在非公平的模式下获取锁,它几乎包括了当前线程在尝试获取锁的时候的所有情况:
1、如果当前锁未被任何线程持有(state=0)则以CAS的方式获取锁,如果获取成功,则将exclusiveOwnerThread设置为当前线程,然后返回成功的结果;若CAS失败,则说明在state=0和CAS获取锁之间,已经有其他的线程获取到该锁,返回失败的结果。
2、如果该锁已经被当前线程所拥有(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数+1(state+1),然后返回成功的结果。因为该线程之前已经获得了锁,故这个累加操作不需要同步。
3、如果该锁已经被其他的线程所拥有(state>0,exclusiveOwnerThread不是当前线程),则直接返回失败的结果。
因为我们用state这个变量来标识该锁被线程重入的次数,因此可以将线程尝试获取锁成功与否可以简化为:state是否成功加1。
获取锁失败的线程如何安全的加入同步队列:addWaiter()
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 首先创建一个新节点,并将当前线程实例封装在内部,mode为null
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;t指向当前队列中的最后一个结点,队列为空则为null
if (t == null) { // Must initialize队列为空
if (compareAndSetHead(new Node()))构造新节点,CAS方式设置为队列首元素,当head为null时更新成功
tail = head;尾指针指向头结点
} else {队列不为空
node.prev = t;
if (compareAndSetTail(t, node)) {CAS将尾指针指向当前节点,当t(原来的尾指针)==tail(当前真实指针)时,执行成功
t.next = node;原尾结点的next指针指向当前节点
return t;
}
}
}
}
这里有两个CAS操作:
compareAndSetHead(new Node())--->CAS方式更新head,仅当原值为null时,更新成功。
compareAndSetTail(t,node)--->CAS方式更新tail,仅当原值为t时,更新成功。
线程加入同步队列后
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);
}
}
在第一个if语句中,首先判断当前线程的前驱结点是否为头结点,如果是,则尝试获取该锁,获取锁成功,则将当前节点设置为头结点(更新头结点)【为什么必须前驱结点为头结点才尝试去获取锁?】因为头结点表示当前占有锁的线程,在正常情况下该线程释放锁之后会通知后面节点中阻塞的线程,阻塞线程被唤醒之后去获取锁。但是还存在另一种情况:就是前驱结点取消了等待,此时当前线程也会被唤醒,这个时候就不应该去唤醒锁,而是应该一直往前回溯,找到一个没有取消等待的结点,然后将自身连在它的后面,一旦我们成功的获取了锁并将自身设置为头结点,就会跳出for循环。否则执行第二个if语句,确保前驱结点的状态为SINGAL,然后阻塞当前线程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
pred状态为SIGNAL,则返回true,表示要阻塞当前线程
return true;
if (ws > 0) {
pred状态为CANCALLED,则一直往队列头部进行回溯,直到找到一个状态不为CANCALLED的结点,并将当前节点node挂在这个节点的后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
pred状态为初始状态,此时通过下面这个方法,将pred状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
其实该方法就是为了确保当前节点的前驱结点状态为SIGNAL,SIGNAL意味着线程释放锁之后会唤醒后面的阻塞线程。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt()方法底层调用了LockSupport.park()方法来阻塞当前线程。
加锁流程总结
解锁的源码相对来说比较简单:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {释放锁(state-1),若释放后锁可以被其他线程获得(state=0),返回true
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;计算待更新的state值
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {待更新的state值为0,说明持有锁的线程未重入,一旦释放锁,其他线程将能获取
free = true;
setExclusiveOwnerThread(null);清除锁的持有线程标记
}
setState(c);更新state值
return free;
}
tryRelease()方法只是将线程持有锁的次数减1,即将state值减1,若减少后,线程将完全释放锁(state=0),则该方法返回true,否则返回false。判断条件:h!=null--->是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒操作;
h.waitStates!=0--->是为了防止队列中虽然有线程,但是该线程还未阻塞,线程在阻塞之前必须将自己的前驱结点状态设置为SIGNAL,否则它不会阻塞自己。
接下来是唤醒线程的操作:unparkSuccessor(),一般情况下只要唤醒后继节点的线程就可以,但是后继节点可能已经取消等待,所以从队列尾部往前回溯,找到离头结点最近的正常节点,并唤醒其线程。
公平锁在源码级别上相比非公平锁的不同:
公平锁模式下,对于锁的获取具有严格的要求。在同步队列有线程等待的条件下,所有线程在获取锁之前必须先加入同步队列。队列中的线程按照加入队列的先后次序获得锁。
final void lock() {
acquire(1);
}
对比非公平,少了非重入式获取锁的方法,这是第一个不同之处。
接着看获取锁的通用方法tryAcquire(),该方法在线程未进入队列,加入到队列阻塞前和阻塞后被唤醒时都会执行。
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;
}
}
在真正CAS获取锁之前加了判断:
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这是判断队列中是否有优先级更高的等待队列,队列中哪个线程的优先级更高?由于头结点是当前获取锁的线程,那么第二个结点代表的线程的优先级更高。
故我们只需要判断队列中第二个结点的是否存在以及第二个结点是否代表当前线程即可。这里分为两种情况进行讨论:
1、第二个结点已经完全插入,但是这个节点是否就是当前线程所在节点还未知,所以通过s.thread!=Thread.currentThread()进行判断,如果为true,说明第二个结点代表其他线程。
2、第二个结点并未完全插入,我们知道结点的插入分为三步:待插入结点的prev指针指向原尾指针;CAS更新尾指针;原尾结点的next指向新插入的结点。
所以(s=h.next)==null就是用来判断CAS更新完尾指针,但是原尾结点的next并未指向新插入的结点,这种情况下,第二个结点必然属于其他线程。
以上的2种情况都会使该方法返回true,即当前有优先级更高的线程在队列中等待,那么当前线程不会执行CAS操作去获取锁,保证了线程获取锁与加入到同步队列中的顺序是一致的。