java中ReentrantLock类的详细介绍(详解)

博主如果看到请联系小白,小白记不清地址了

简介

ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

ReentrantLock的类图如下:

java中ReentrantLock类的详细介绍(详解)

ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。如果在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。

事实上,公平锁往往没有非公平锁的效率高,但是,并不是任何场景都是以TPS作为唯一指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越能够得到优先满足。

下面我们着重分析ReentrantLock是如何实现重进入和公平性获取锁的特性,并通过测试来验证公平性对性能的影响。

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题:

线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;

锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。

ReentrantLock是通过自定义同步器来实现锁的获取与释放,我们以非公平锁(默认)实现为例,对锁的获取和释放进行详解。

获取锁

ReentrantLock的默认构造函数为:

public ReentrantLock() {
sync = new NonfairSync();
}

即内部同步组件为非公平锁,获取锁的代码为:

public void lock() {
sync.lock();
}

通过简介中的类图可以看到,Sync类是ReentrantLock自定义的同步组件,它是ReentrantLock里面的一个内部类,它继承自AQS,它有两个子类:公平锁FairSync和非公平锁NonfairSync。ReentrantLock的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的lock()方法:

final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

该程序首先会通过compareAndSetState(int, int)方法来尝试修改同步状态,如果修改成功则表示获取到了锁,然后调用setExclusiveOwnerThread(Thread)方法来设置获取到锁的线程,该方法是继承自AbstractOwnableSynchronizer类,AQS继承自AOS类,它的主要作用就是记录获取到独占锁的线程,AOS类的定义很简单:

public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L; protected AbstractOwnableSynchronizer() { } // The current owner of exclusive mode synchronization.
private transient Thread exclusiveOwnerThread; protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
} protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}

如果同步状态修改失败,则表示没有获取到锁,需要调用acquire(int)方法,该方法定义在AQS中,如下:

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int)是子类需要重写的方法,在非公平锁中的实现如下: protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 同步状态为0,表示没有线程获取锁
if (c == 0) {
// 尝试修改同步状态
if (compareAndSetState(0, acquires)) {
// 同步状态修改成功,获取到锁
setExclusiveOwnerThread(current);
return true;
}
}
// 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程
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;
}

nonfairTryAcquire(int)方法首先判断同步状态是否为0,如果是0,则表示该锁还没有被线程持有,然后通过CAS操作获取同步状态,如果修改成功,返回true。如果同步状态不为0,则表示该锁已经被线程持有,需要判断当前线程是否为获取锁的线程,如果是则获取锁,成功返回true。成功获取锁的线程再次获取该锁,只是增加了同步状态的值,这也就实现了可重入锁。

释放锁

成功获取锁的线程在完成业务逻辑之后,需要调用unlock()来释放锁:

public void unlock() {
sync.release(1);
}
unlock()调用NonfairSync类的release(int)方法释放锁,release(int)方法是定义在AQS中的方法: 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(int)是子类需要实现的方法: protected final boolean tryRelease(int releases) {
// 计算新的状态值
int c = getState() - releases;
// 判断当前线程是否是持有锁的线程,如果不是的话,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 新的状态值是否为0,若为0,则表示该锁已经完全释放了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新状态值
setState(c);
return free;
}

如果该锁被获取n次,那么前(n-1)次tryRelease(int)方法必须返回false,只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当状态为0时,将占有线程设为null,并返回true,表示释放成功。

公平锁与非公平锁

公平性与否是针对锁获取顺序而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合FIFO原则。我们在前面介绍了非公平锁NonfairSync调用的nonfairTryAcquire(int)方法,在该方法中,只要通过CAS操作修改同步状态成功,则当前线程就获取到了锁,而公平锁则不同,公平锁FairSync的tryAcquire(int)方法如下所示:

protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 同步状态为0,表示没有线程获取锁
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程
else if (current == getExclusiveOwnerThread()) {
// 获取锁的线程是当前线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
该方法与nonfairTryAcquire(int)方法比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,该方法定义如下: public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
// 同步队列尾节点
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,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

下面我们编写一个测试程序来观察公平锁和非公平锁在获取锁时的区别:

public class FairAndUnfairTest {
private static CountDownLatch start; private static class MyReentrantLock extends ReentrantLock {
public MyReentrantLock(boolean fair) {
super(fair);
} public Collection<Thread> getQueuedThreads() {
List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
Collections.reverse(arrayList);
return arrayList;
}
} private static class Worker extends Thread {
private Lock lock; public Worker(Lock lock) {
this.lock = lock;
} @Override
public void run() {
try {
start.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 连续两次打印当前的Thread和等待队列中的Thread
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println("Lock by [" + getName() + "], Waiting by " + ((MyReentrantLock) lock).getQueuedThreads());
} finally {
lock.unlock();
}
}
} public String toString() {
return getName();
}
} public static void main(String[] args) {
Lock fairLock = new MyReentrantLock(true);
Lock unfairLock = new MyReentrantLock(false); testLock(fairLock);
// testLock(unfairLock);
} private static void testLock(Lock lock) {
start = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
Thread thread = new Worker(lock);
thread.setName("" + i);
thread.start();
}
start.countDown();
}
}
testLock(fairLock)运行结果(不唯一):
Lock by [0], Waiting by [4, 1, 2, 3]
Lock by [4], Waiting by [1, 2, 3, 0]
Lock by [1], Waiting by [2, 3, 0, 4]
Lock by [2], Waiting by [3, 0, 4, 1]
Lock by [3], Waiting by [0, 4, 1, 2]
Lock by [0], Waiting by [4, 1, 2, 3]
Lock by [4], Waiting by [1, 2, 3]
Lock by [1], Waiting by [2, 3]
Lock by [2], Waiting by [3]
Lock by [3], Waiting by []
testLock(unfairLock)运行结果(不唯一): Lock by [0], Waiting by [1]
Lock by [0], Waiting by [1, 2, 3, 4]
Lock by [1], Waiting by [2, 3, 4]
Lock by [1], Waiting by [2, 3, 4]
Lock by [2], Waiting by [3, 4]
Lock by [2], Waiting by [3, 4]
Lock by [3], Waiting by [4]
Lock by [3], Waiting by [4]
Lock by [4], Waiting by []
Lock by [4], Waiting by []

从上述结果可以看到,公平锁每次都是队列中的第一个节点获取到锁,而非公平锁出现了一个线程连续获取锁的情况。

为什么会出现连续获取锁的情况呢?因为在nonfairTryAcquire(int)方法中,每当一个线程请求锁时,只要获取了同步状态就成功获取了锁。在此前提下,刚刚释放锁的线程再次获取到同步状态的几率很大,而其他线程只能在同步队列中等待。

非公平锁有可能使线程饥饿,那为什么还要将它设置为默认模式呢?我们再次观察上面的运行结果,如果把每次不同线程获取到锁定义为1次切换,公平锁在测试中进行了10次切换,而非公平锁只有5次切换,这说明非公平锁的开销更小。我们下面再进行一个测试(还是用上面的程序,不过使用了10个线程,每个线程获取2000次锁,程序运行环境为Centos7.3 E5-2682 2.50GHz 单核 2GB),通过vmstat统计测试程序上下文切换次数,运行结果如下所示:

公平锁

java中ReentrantLock类的详细介绍(详解)

程序运行总耗时为5308毫秒

非公平锁

java中ReentrantLock类的详细介绍(详解)

程序运行总耗时为3176毫秒

从结果中可以看到,公平锁与非公平锁相比,耗时更多,线程上下文切换次数更多。可以看出,公平锁保证了锁的获取按照FIFO原则,而代价则是进行大量的线程切换。非公平锁虽然可能导致线程饥饿,但却有极少的线程切换,保证了其更大的吞吐量。

上一篇:java中Timer类的详细介绍(详解)


下一篇:java中Condition类的详细介绍(详解)