保证线程安全的方式有很多,比如CAS操作、synchronized、原子类、volatile保证可见性和ReentrantLock等,这篇文章我们主要探讨ReentrantLock的相关内容。本文基于JDK1.8讲述ReentrantLock.
一、可重入锁
所谓可重入锁,即一个线程已经获得了某个锁,当这个线程要再次获取这个锁时,依然可以获取成功,不会发生死锁的情况。synchronized就是一个可重入锁,除此之外,JDK提供的ReentrantLock也是一种可重入锁。
二、ReentrantLock
2.1 ReentrantLock的简单使用
public class TestReentrantLock {
private static int i = 0;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
i++;
} finally {
lock.unlock();
}
System.out.println(i);
}
}
上面是ReentrantLock的一个简单使用案列,进入同步代码块之前,需要调用lock()方法进行加锁,执行完同步代码块之后,为了防止异常发生时造成死锁,需要在finally块中调用unlock()方法进行解锁。
2.2 ReentrantLock UML图
2.3 lock()方法调用链
上图描述了ReentrantLock.lock()加锁的方法调用过程。在ReentrantLock中有一个成员变量private final Sync sync,Sync是AQS的一个子类。ReentrantLock的lock()方法中,调用了sync的lock()方法,这个方法为抽象方法,具体调用的是NonfairSync中实现的lock()方法:
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
在这个方法中,先尝试通过CAS操作进行加锁。如果加锁失败,会调用AQS的acquire()方法:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在AQS的acquire方法中,先尝试调用tryAcquire方法进行加锁,如果失败,会调用acquireQueued进入等待队列当中。acquireQueued方法将会在第三章中讲解,先来看tryAcquire方法的内容。AQS的tryAcquire方法是一个模板方法,其具体实现在NonfairSync的tryAcquire方法中,里面仅仅是调用了nonfairTryAcquire方法:
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
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;
}
在这个方法中,先获取state判断其是否为0。如果为0表示没有其他线程占用锁,会尝试通过CAS操作将state设为1进行加锁;如果state不为0,表示某个线程已经占用了锁,判断占用锁的线程是否为当前线程,如果是,则将state进行加1的操作,这就是ReentrantLock可重入的实现原理。
三、AQS
AQS即AbstractQueuedSynchronizer。AQS提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。AQS其实是CLH(Craig,Landin,Hagersten)锁的一个变种,下面来讲解AQS的核心思想及其具体实现。
3.1 state
/**
* The synchronization state.
*/
private volatile int state;
state是AQS中最核心的成员变量。这是一个volatile变量,当其为0时,表示没有任何线程占用锁。线程通过CAS将state从0置为1进行加锁,当线程持有锁的情况下,再次进行加锁,会将state加1,即重入。
3.2 exclusiveOwnerThread
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中的成员变量,其作用是实现可重入机制时,用于判断持有锁的线程是否为当前线程。
3.3 AQS等待队列
除了以上state和exclusiveOwnerThread两个重要的成员变量以外,AQS还维护了一个等待队列。当线程尝试加锁失败时,会进入这个等待队列中,这也是整个AQS中最核心的内容。这个等待队列是一个双向链表,其节点Node对等待加锁的线程进行封装。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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;
// 通过CAS操作将自身追加到链表尾部
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
当线程尝试加锁失败时,通过CAS操作将自身追加到链表尾部。入队之后,会调用acquireQueued在队列中尝试加锁:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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);
}
}
在这个方法中,会判断其前置节点是否为头节点,如果是,则尝试进行加锁。如果加锁失败,则调用LockSupport.park方法进入阻塞状态,等待其前置节点释放锁之后将其唤醒。
3.4 AQS中的模板方法设计模式
AQS完美地运用了模板方法设计模式,其中定义了一系列的模板方法。比如以下方法:
// 互斥模式下使用:尝试获取锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 互斥模式下使用:尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试获取锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试释放锁
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
这些方法在AQS中只抛出了UnsupportedOperationException异常,所以需要子类去实现它们。之所以没有将这些方法设计成为抽象方法,是因为AQS的子类可能只需要实现其中的某些方法即可实现其功能。
总结
不同版本的JDK,AQS的实现可能会有细微的差异,但其核心思想是不会变的,即线程加锁失败后,通过CAS进行入队的操作,并通过CAS的方法设置state来获得锁。
到这里,本篇关于Java多线程中的可重入锁ReentrantLock详细讲解的文章就到此结束了,如果您还想要了解更多关于Java多线程相关知识,推荐您可以看一看我们的Java多线程视频,适合想要了解Java多线程的小伙伴们。