类图结构
ReentrantLock 是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的 AQS 阻塞队列里面。首先看下 ReentrantLock 的类图以便对它的实现有个大致了解,如图 6-4 所示。
从类图可以看到,ReentrantLock 最终还是使用 AQS 来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
其中 Sync 类直接继承自 AQS,它的子类 NonfairSync 和 FairSync 分别实现了获取锁的非公平与公平策略。
在这里,AQS 的 state 状态值表示线程获取该锁的可重入次数,在默认情况下,state 的值为 0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用 CAS 设置 state 的值为 1,如果 CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为 2,这就是可重入次数。在该线程释放该锁时,会尝试使用 CAS 让状态值减 1,如果减 1 后状态值为 0,则当前线程释放该锁。
获取锁
1.void lock() 方法
当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。
public void lock() {
sync.lock();
}
在如上代码中,ReentrantLock 的 lock()委托给了 sync 类,根据创建 ReentrantLock 构造函数选择 sync 的实现是 NonfairSync 还是 FairSync,这个锁是一个非公平锁或者公平锁。这里先看 sync 的子类 NonfairSync 的情况,也就是非公平锁时。
final void lock() {
//(1)CAS 设置状态值
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//(2)调用 AQS 的 acquire 方法
acquire(1);
}
在代码(1)中,因为默认 AQS 的状态值为 0,所以第一个调用 Lock 的线程会通过 CAS 设置状态值为 1,CAS 成功则表示当前线程获取到了锁,然后 setExclusiveOwnerThread 设置该锁持有者是当前线程。
如果这时候有其他线程调用 lock 方法企图获取该锁,CAS 会失败,然后会调用 AQS 的 acquire 方法。注意,传递参数为 1,这里再贴下 AQS 的 acquire 的核心代码。
public final void acquire(int arg) {
//(3)调用 ReentrantLock 重写的 tryAcquire 方法
if (! tryAcquire(arg) &&
// tryAcquiref 返回 false 会把当前线程放入 AQS 阻塞队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
之前说过,AQS 并没有提供可用的 tryAcquire 方法,tryAcquire 方法需要子类自己定制化,所以这里代码(3)会调用 ReentrantLock 重写的 tryAcquire 方法。我们先看下非公平锁的代码。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//(4)当前 AQS 状态值为 0
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//(5)当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}//(6)
return false;
}
首先代码(4)会查看当前锁的状态值是否为 0,为 0 则说明当前该锁空闲,那么就尝试 CAS 获取该锁,将 AQS 的状态值从 0 设置为 1,并设置当前锁的持有者为当前线程然后返回,true。如果当前状态值不为 0 则说明该锁已经被某个线程持有,所以代码(5)查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加 1,然后返回 true,这里需要注意,nextc<0 说明可重入次数溢出了。如果当前线程不是锁的持有者则返回 false,然后其会被放入 AQS 阻塞队列。
介绍完了非公平锁的实现代码,回过头来看看非公平在这里是怎么体现的。首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。
这里假设线程 A 调用 lock()方法时执行到 nonfairTryAcquire 的代码(4),发现当前状态值不为 0,所以执行代码(5),发现当前线程不是线程持有者,则执行代码(6)返回 false,然后当前线程被放入 AQS 阻塞队列。
这时候线程 B 也调用了 lock()方法执行到 nonfairTryAcquire 的代码(4),发现当前状态值为 0 了(假设占有该锁的其他线程释放了该锁),所以通过 CAS 设置获取到了该锁。明明是线程 A 先请求获取该锁呀,这就是非公平的体现。这里线程 B 在获取锁前并没有查看当前 AQS 队列里面是否有比自己更早请求该锁的线程,而是使用了抢夺策略。那么下面看看公平锁是怎么实现公平的。公平锁的话只需要看 FairSync 重写的 tryAcquire 方法。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//(7)当前 AQS 状态值为 0
if (c == 0) {
//(8)公平性策略
if (! hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//(9)当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error(「Maximum lock count exceeded」);
setState(nextc);
return true;
}//(10)
return false;
}
如以上代码所示,公平的 tryAcquire 策略与非公平的类似,不同之处在于,代码(8)在设置 CAS 前添加了 hasQueuedPredecessors 方法,该方法是实现公平性的核心代码,代码如下。
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());
}
在如上代码中,如果当前线程节点有前驱节点则返回 true,否则如果当前 AQS 队列为空或者当前线程节点是 AQS 的第一个节点则返回 false。其中如果 h==t
则说明当前队列为空,直接返回 false;如果h!=t
并且s==null
则说明有一个元素将要作为 AQS 的第一个节点入队列(回顾前面的内容,enq 函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后面),那么返回 true,如果 h!=t
并且 s!=null
和 s.thread != Thread.currentThread()
则说明队列里面的第一个元素不是当前线程,那么返回 true。
2.void lockInterruptibly() 方法
该方法与 lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的 interrupt()方法,则当前线程会抛出 InterruptedException 异常,然后返回。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果当前线程被中断,则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取资源
if (! tryAcquire(arg))
//调用 AQS 可被中断的方法
doAcquireInterruptibly(arg);
}
3.boolean tryLock() 方法
尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回 true,否则返回 false。注意,该方法不会引起当前线程阻塞。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
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;
}
如上代码与非公平锁的 tryAcquire()方法代码类似,所以 tryLock()使用的是非公平策略。
4.boolean tryLock(long timeout, TimeUnit unit)方法
尝试获取锁,与 tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回 false。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
//调用 AQS 的 tryAcquireNanos 方法
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
释放锁
1.void unlock() 方法
尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的 AQS 状态值减 1,如果减去 1 后当前状态值为 0,则当前线程会释放该锁,否则仅仅减 1 而已。如果当前线程没有持有该锁而调用了该方法则会抛出 IllegalMonitorStateException 异常,代码如下。
public void unlock() {
sync.release(1);
}
protected final boolean tryRelease(int releases) {
//(11)如果不是锁持有者调用 UNlock 则抛出异常
int c = getState() - releases;
if (Thread.currentThread() ! = getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//(12)如果当前可重入次数为 0,则清空锁持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//(13)设置可重入次数为原始值-1
setState(c);
return free;
}
如代码(11)所示,如果当前线程不是该锁持有者则直接抛出异常,否则查看状态值是否为 0,为 0 则说明当前线程要放弃对该锁的持有权,则执行代码(12)把当前锁持有者设置为 null。如果状态值不为 0,则仅仅让当前线程对该锁的可重入次数减 1。
案例介绍
下面使用 ReentrantLock 来实现一个简单的线程安全的 list。
public static class ReentrantLockList {
//线程不安全的 list
private ArrayList<String> array = new ArrayList<String>();
//独占锁
private volatile ReentrantLock lock = new ReentrantLock();
//添加元素
public void add(String e) {
lock.lock();
try {
array.add(e);
} finally {
lock.unlock();
}
}
//删除元素
public void remove(String e) {
lock.lock();
try {
array.remove(e);
} finally {
lock.unlock();
}
}
//获取数据
public String get(int index) {
lock.lock();
try {
return array.get(index);
} finally {
lock.unlock();
}
}
如上代码通过在操作 array 元素前进行加锁保证同一时间只有一个线程可以对 array 数组进行修改,但是也只能有一个线程对 array 元素进行访问。
同样最后使用图(见图 6-5)来加深理解。
如图 6-5 所示,假如线程 Thread1、Thread2 和 Thread3 同时尝试获取独占锁 ReentrantLock,假设 Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点并被放入 ReentrantLock 对应的 AQS 阻塞队列,而后被阻塞挂起。
如图 6-6 所示,假设 Thread1 获取锁后调用了对应的锁创建的条件变量 1,那么 Thread1 就会释放获取到的锁,然后当前线程就会被转换为 Node 节点插入条件变量 1 的条件队列。由于 Thread1 释放了锁,所以阻塞到 AQS 队列里面的 Thread2 和 Thread3 就有机会获取到该锁,假如使用的是公平策略,那么这时候 Thread2 会获取到该锁,从而从 AQS 队列里面移除 Thread2 对应的 Node 节点。
小结
本节介绍了 ReentrantLock 的实现原理,ReentrantLock 的底层是使用 AQS 实现的可重入独占锁。在这里 AQS 状态值为 0 表示当前锁空闲,为大于等于 1 的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。另外,由于该锁是独占锁,所以某时只有一个线程可以获取该锁。