Java多线程之深入理解ReentrantLock

 更新时间:2021年04月25日 17:13:39   作者:EileenChang  
这篇文章主要介绍了Java多线程之深入理解ReentrantLock,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下

前言

保证线程安全的方式有很多,比如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 ReentrantLock总结内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 微信小程序完整项目实战记录(前端+SpringBoot后端)

    微信小程序完整项目实战记录(前端+SpringBoot后端)

    随着微信小程序的流行,越来越多的开发者开始涉足小程序开发,下面这篇文章主要给大家介绍了关于微信小程序完整项目实战的相关资料,项目包括前端+SpringBoot后端,需要的朋友可以参考下
    2024-09-09
  • JVM 命令行工具的使用

    JVM 命令行工具的使用

    造成Java应用出现性能问题的因素非常多,想要定位这些问题,一款优秀的性能诊断工具必不可少,本文主要介绍了JVM 命令行工具的使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-04-04
  • SpringBoot实现反向代理的示例代码

    SpringBoot实现反向代理的示例代码

    本文主要介绍了SpringBoot实现反向代理的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • Spring Boot高效数据聚合之道深入讲解

    Spring Boot高效数据聚合之道深入讲解

    这篇文章主要给大家介绍了关于Spring Boot高效数据聚合之道的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Spring Boot具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-06-06
  • spring如何动态指定具体实现类

    spring如何动态指定具体实现类

    这篇文章主要为大家详细介绍了spring如何动态指定具体实现类,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • java中常见的死锁以及解决方法代码

    java中常见的死锁以及解决方法代码

    这篇文章主要介绍了java中常见的死锁以及解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • Restful之通用返回格式类设计

    Restful之通用返回格式类设计

    这篇文章主要介绍了Restful之通用返回格式类设计,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • idea 设置鼠标悬停(放上)弹出注释的方法

    idea 设置鼠标悬停(放上)弹出注释的方法

    这篇文章主要介绍了idea 设置鼠标悬停(放上)弹出注释的方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Java的StringBuilder在高性能场景下的正确用法

    Java的StringBuilder在高性能场景下的正确用法

    StringBuilder 对字符串的操作是直接改变字符串对象本身,而不是生成新的对象,所以新能开销小.与StringBuffer相比StringBuilder的性能略高,StringBuilder则没有保证线程的安全,从而性能略高于StringBuffer,需要的朋友可以参考下
    2023-05-05
  • 如何解决org.apache.jasper.JasperException:无法为JSP编译类详解

    如何解决org.apache.jasper.JasperException:无法为JSP编译类详解

    这篇文章主要给大家介绍了关于如何解决org.apache.jasper.JasperException:无法为JSP编译类的相关资料,原因可能是JSP文件的语法错误、类路径问题或其他配置问题,建议检查JSP文件的语法、类路径配置和其他相关配置,需要的朋友可以参考下
    2023-06-06

最新评论