友情提示:本文环境是jdk1.8,阅读本文需要对AQS和CMS有一定的了解,不然可能会有点吃力,不过应该也能看懂,我对涉及到AQS的部分有做相关的解释
谈起java并发,Lock和synchronized关键字肯定是避免不了的,两者都可用于并发环境下同步控制线程的安全性,无论是在日常使用还是面试中,都是十分常见的。今天先来品一品ReentrantLock(可重入锁),Lock的实现类之一,可重入指的是一个线程能够对一个临界资源重复加锁。
ReentrantLock构造函数和成员变量
先看一下ReentrantLock的成员变量,很简单,就一个Sync变量,Sync是ReentrantLock的一个抽象内部类,它继承了AbstractQueuedSynchronizer(AQS),同时ReentrantLock类里还有NofairSync和FairSync两个内部类,这两个内部类又都继承了Sync,我们所创建的ReentrantLock对象是公平锁还是非公平锁就是通过这两个类来控制的,通过这两个类的名字就能看得出来。
private final Sync sync;
看 ReentrantLock的构造方法,一目了然
//无参构造函数创建的是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//我们也可以通过有参构造函数来创建公平锁或非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock的方法
我们在使用锁的时候,最多的就是使用lock和unlock这两个方法了吧,下面通过源码来看看它们是怎么实现的
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
可以看到,这两个方法都是直接调用了成员变量sync的方法实现的,通过前面的构造函数我们知道sync变量的值取决于我们创建的是公平锁(FairSync)还是非公平锁(NonfairSync)。
注意,前文说到Sync继承了AQS,所以无论是NonfairSync还是FairSync的方法,都会涉及到AQS的成员变量和方法,我这边先简单提一下,以免下面代码不好理解。
AQS有一个state的私有变量,该变量用于记录锁的状态,0代表锁没有被线程占用,1代表有线程持有当前锁,该值可以大于1,因为锁是可重入的,每次重入都加上1。
AQS有一个阻塞队列,用于存放没有获得到锁的线程,注意,该线程的头部节点为获得锁的节点,是不算在阻塞队列中的(感觉有点拗口,但这非常重要)
lock方法实现
先来看NonfairSync内部lock的实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//尝试用CAS操作去设置State的值,将0改成1
if (compareAndSetState(0, 1))
//如果CAS操作成功,将当前线程设置为正在执行的线程,代表该线程成功获得锁
setExclusiveOwnerThread(Thread.currentThread());
else
//CAS操作失败,调用acquire,该方法是AQS的方法,我把它放在下面
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
//该方法的实现在Sync类中
return nonfairTryAcquire(acquires);
}
}
//先走第一个判断条件tryAcquire再次尝试获取锁,如果获取失败返回false,执行acquireQueued
//acquireQueued和addWaiter都是AQS的方法
//当线程没有获得锁时会调用它们将线程添加到阻塞队列,我将源码放在下面
public final void acquire(int arg){
if(!tryAcquire(arg)&&
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
下面这一段代码是线程获得锁失败时,AQS将线程加入到阻塞队列并将其挂起的实现,对AQS没有了解的同学 读起来可能比较吃力,可以自己打开AQS源码配合着一起看,或者先跳过,但是AQS是java并发的基础,希望大家或多或少的去了解一下。
//addWaiter方法用于将线程加入到阻塞队列中
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果队列不为空
if (pred != null) {
// 将当前的队尾节点设置为自己的前节点
node.prev = pred;
//通过CAS将自己设置为新的队尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队列为空或者CAS操作失败(有线程竞争入队列)将会调用此方法
enq(node);
return node;
}
// 采用自旋的方式入队
// 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
// 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
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;
}
}
}
}
//前面的方法是将线程添加到阻塞队列中,而将线程挂起是由acquireQueued实现的
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获得当前线程节点的前节点
final Node p = node.predecessor();
//判断前节点是否为头部节点,是的话调用tryAcquire尝试获得锁
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);
}
}
nonfairTtyAcquire这个方法存在于Sync类中,我这里无法理解为什么要这么设计,而不是像FairSync类一样直接将实现写在tryAcquire方法中,这样调用NonfairSync的tryAcquire时还需要再去调用一次Sync的nonfairTtyAcquire,如果有懂的小伙伴欢迎在下面留言告诉我。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取state的值
int c = getState();
//如果c=0,即当前没有线程获得锁,用CAS尝试去将state的值修改成acquires
//如果修改成功,则当前线程成功获得锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//修改不成功,判断当前获得锁的线程和该线程是否为同一个线程(可重入的实现)
//如果是,则将state+1,否则返回false
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;
}
简单总结一下非公平锁的lock实现原理,线程1调用lock方法,此时锁还没被任何线程占用,state为0,线程1会通过CAS将state从0设置为1,表示当前锁已经被它所占用,此时线程2也调用了lock方法,线程2会通过CAS尝试将state从0改成1,但是此时的state已经为1了,所以这里失败了,接着往下走,线程2会去判断它 是否和线程1是同一个线程,如果是,那么线程2可以获得锁,并将state+1(这就是可重入锁),如果不是同一个线程,线程2会作为尾节点加入到阻塞队列,然后再将线程2挂起,等待线程1释放锁并将其唤醒。
FairSync内部lock实现
FairSync的lock方法实现大部分和NonfairSync的相同,只是少了一开始就通过CAS去尝试修改state的值(这就是插队,如果这时候锁刚好被释放,那么线程就能直接获取到锁,这就是非公平锁和公平锁的区别),代码和上面的基本一样,应该很容易看懂
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//可以看到,公平锁这里没有一上来就用CAS操作去获得锁,而是调用了acquire
final void lock() {
acquire(1);
}
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;
}
}
unlock方法实现
unlock方法比较简单,无论是公平锁还是非公平锁,他们的调用的都是release(1),release方法的实现在AQS类中
public void unlock() {
sync.release(1);
}
public final boolean release(int arg){
//Sync类重写了tryRelease方法
if(tryRelease(arg){
//tryRelease返回true,则说明该线程成功释放锁,AQS队列将删除对应的节点
Node h=head;
if(h!=null && h.waitStatus!=0)
unparkSuccessor(h);
return true;
}
return false;
}
//Sync重写的tryRelease方法实现
protected final boolean tryRelease(int releases){
//获得state的值并将其减去releases
int c = getState() - releases;
if(Thread.currentThread() != getExclusiveOwnerThread())
throw new IllealMonitorStateException();
boolean free = flase;
//当c==0的时候才会将free设置为true,此时没有线程占有锁
//所以调用setExclusiveOwnerThread设置exclusiveOwnerThread为空
if (c == 0){
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看到,只有当c==0时,才能将state的值修改为0,这和上面一个线程调用lock时state值的叠加是相对应的,所有一个线程想要释放可重入锁,它需要调用和lock相同次数的unlock,才能成功的将锁释放。
至此,ReentrantLock的lock和unlock方法就讲完了,这两个方法也是这个类最常用的两个方法,由于其他的方法都比较的简单,这里就不再继续展开讲,有兴趣的同学可以自行去阅读源码,如果你已经理解了上面的内容,应该很容易读懂其他的几个方法。
由于作者能力有限,如果有什么没看懂或者文章有什么错误的地方,欢迎在下面留言讨论。