带你深入了解Java多线程中的可重入锁ReentrantLock

撸串研究生 2021-08-20 10:50:48 浏览数 (2118)
反馈

保证线程安全的方式有很多,比如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多线程的小伙伴们。


0 人点赞