深度解析Java中的ReentrantLock原理

 更新时间:2023年07月20日 10:03:09   作者:我是小趴菜  
这篇文章主要介绍了关于ReentrantLock的原理解析,文章通过代码示例介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下

在高并发编程中,AbstractQueuedSynchronizer(简称AQS)抽象的队列同步器是我们必须掌握的,AQS底层提供了二种锁模式

  • 独占锁:ReentrantLock就是基于独占锁模式实现的
  • 共享锁:CountDownLatch,ReadWriteLock,Semplere都是基于共享锁模式实现的

接下来我们通过ReentrantLock底层实现原理来了解AQS独占锁模式的实现原理

ReentrantLock用法

我们创建三个线程去获取资源,然后执行业务逻辑

ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
    try{
        lock.lock();
        TimeUnit.SECONDS.sleep(10);
    }catch (Exception e){
    }finally {
        lock.unlock();
    }
},"t1").start();
new Thread(() -> {
    try{
        lock.lock();
        TimeUnit.SECONDS.sleep(10);
    }catch (Exception e){
    }finally {
        lock.unlock();
    }
},"t2").start();
new Thread(() -> {
    try{
        lock.lock();
        TimeUnit.SECONDS.sleep(10);
    }catch (Exception e){
    }finally {
        lock.unlock();
    }
},"t3").start();

首先分析一下获取锁的原理,也就是 lock.lock()方法

public void lock() {
    //继续进入这个方法
    sync.lock();
}

进入这个方法的时候有两个实现类,一个是公平锁FairSync,还有一个就是非公平锁NonfairSync,由于非公平锁比公平锁复杂,所以我们先分析非公平锁的原理

final void lock() {
    //这里就会尝试去获取锁,如果成功获取到了锁,此时state的值就会从0变成1
    if (compareAndSetState(0, 1))
        //就把exclusiveOwnerThread的值设置成当前线程
        //exclusiveOwnerThread是AQS里面的一个变量,也就是线程获取到了锁之后,就会把这个值设置成当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果没有获取到锁,就执行下面这个方法
        acquire(1);
}

进入这个方法的时候有两个实现类,一个是公平锁FairSync,还有一个就是非公平锁NonfairSync,由于非公平锁比公平锁复杂,所以我们先分析非公平锁的原理

final void lock() {
    //这里就会尝试去获取锁,如果成功获取到了锁,此时state的值就会从0变成1
    if (compareAndSetState(0, 1))
        //就把exclusiveOwnerThread的值设置成当前线程
        //exclusiveOwnerThread是AQS里面的一个变量,也就是线程获取到了锁之后,就会把这个值设置成当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果没有获取到锁,就执行下面这个方法
        acquire(1);
}

第一个线程进来之后,由于state等于0,那么就可以获取到锁,于是就会把state的值设置成1,然后exclusiveOwnerThread值设置成自己的线程

这时候第二个线程进来,发现state的值已经是1了,所以就会进入到acquire(1);这个方法了

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里一共有三个方法,我们先分析tryAcquire(arg);这里的参数值等于1

//还是进入到非公平锁的实现类NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    //拿到当前线程
    final Thread current = Thread.currentThread();
    //获取到state的值,由于之前已经有线程获取到锁了,所以这个值现在等于1,也就不会进入带if分支
    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;
    }
    //所以最后返回false
    return false;
}

所以tryAcquire(arg)方法做了以下几个步骤

  • 1:继续尝试获取锁,如果获取到锁了就直接返回
  • 2:如果没有获取到锁,就判断此时获取到锁的线程是不是自己,如果是,就可以获取到资源继续执行,也就是可重入锁的原理
  • 3:如果以上二个步骤都不符合,就直接返回false

第一个tryAcquire(arg)方法最后返回false,但是这里是取返,所以是为true,所以就会进入acquireQueued()方法,但是在这里还有个addWaiter(Node.EXCLUSIVE), arg)方法,所以我们先分析这个方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE表示是独占模式
        selfInterrupt();
}
private Node addWaiter(Node mode) {
    //将当前线程封装成一个Node节点,并设置成独占模式,
    //注意:一个新的Node节点的waitStatus的值等于0
    Node node = new Node(Thread.currentThread(), mode);
    //在第一次进来的时候,tail和head都是null,所以不会进入到if分支里面去
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //由于head和tail都为null,所以会进入初始化链表的方法
    //初始化链表
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {  //注意:这里是死循环
        Node t = tail;
        //第一次进来,因为tail=null,所以会进入到if里面去
        if (t == null) { // Must initialize
            //这里新创建一个空的Node节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • 第一次进来:因为第一次进来的时候tail=null,所以会进入到if中去,然后创建一个新的空的节点,然后将头节点和尾节点都指向这个节点

  • 然后进入第二次循环:这时候tail已经不为空了,所以会进入到else分支里面去,所以的操作就是将当前线程封装成的Node设置尾巴节点,然后设置前置节点和后置节点的关系

这时候第三个线程进来,然后假设也没有获取到锁,那么也会进入到addWaiter()方法中

private Node addWaiter(Node mode) {
    //将当前线程封装成一个Node节点,并设置成独占模式,
    //注意:一个新的Node节点的waitStatus的值等于0
    Node node = new Node(Thread.currentThread(), mode);
    //这时候tail和head都不为null了,所以就会进入if分支
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //由于tail和head都不为null,也就不会执行到这里来了
    enq(node);
    return node;
}

这时候新进来的线程就会从链表尾部插入,然后重新更新tail尾节点指向当前线程

这时候addWaiter(Node.EXCLUSIVE), arg);就会返回一个Node节点了,这个Node节点就是当前封装了当前线程的Node,比如现在第一个线程Thread-1获取到锁了,然后Thread-2进来,那么Thread-2就要进入队列中进行等待了,所以这时候返回的就是Thread-2这个Node节点。同理,第三个线程Thread-3进来,那么这时候返回的就是Thread-3这个Node节点

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE表示是独占模式
        selfInterrupt();
}

现在我们先分析第二个线程Thread-2。进入acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) {
    //此时Node节点就是Thread-2这个线程的Node
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //拿到上一个节点,因为Thread-2是第一个排队的线程,所以他的前置节点就是头节点
            final Node p = node.predecessor();
            //p是头节点,所以会进入tryAcquire(arg)方法,这个方法就是再次去尝试获取锁
            //但是头节点就是个虚拟节点,是不可能获取到锁的,所以不会进入到if分支里面去
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //此时会进入到这个分支里面去,此时的p是头节点,node是Thread-2的Node节点
            //在第二次循环过后,shouldParkAfterFailedAcquire(p, node)会返回true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
//第一次进来,由于头节点是刚初始化的,所以waitStatus=0,所以会进入到else分支,修改头节点waitStatus
//然后由于外面的是死循环,所以会再次进入,此时头节点的waitStatus的值是Node.SIGNAL,所以会直接返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将头节点的waitStatus修改成Node.SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

由于shouldParkAfterFailedAcquire(p, node)返回true了,所以就会继续执行 parkAndCheckInterrupt()方法了

private final boolean parkAndCheckInterrupt() {
    //将Thread-2线程阻塞挂起,这里为什么是this呢,因为现在执行进来的就是Thread-2线程
    LockSupport.park(this);
    return Thread.interrupted();
}

后续Thread-3线程进来之后,也会在这里阻塞住,然后等待被唤醒

现在Thread-1线程业务执行结束了,然后就要释放锁

lock.unlock();
public void unlock() {
    sync.release(1);
}
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(arg)方法

protected final boolean tryRelease(int releases) {
    //将state的值减去1
    int c = getState() - releases;
    //判断当前线程是否是获取锁的那个线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果state的值减1之后变成0了,那么就将exclusiveOwnerThread设置成null,
    //因为我们state的值就是等于1,所以c=0,就会进入if分支里面去
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //将state的值设置成c
    setState(c);
    //因为会进入if分支,所以此时free的值等于true
    return free;
}

所以tryRelease(arg)最后会返回true,于是就会进入if分支里面去

public final boolean release(int arg) {
    if (tryRelease(arg)) { //返回true
        //拿到链表的头节点
        Node h = head;
        //虽然在初始化的时候Node节点的waitStatus的值等于0,但是后面进来的线程会将前置节点的该值修改成-1
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//node是头节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    //拿到头节点的下一个节点,也就是Thread-2线程的节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //最后唤醒Thread-2线程继续执行吧
        LockSupport.unpark(s.thread);
}

Thread-2之前是在acquireQueued()方法中被阻塞住的

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //当再次循环到这里来的时候,Thread-2的前置节点就是头节点,并且此时Thread-1线程
            //已经释放锁了,所以此时tryAcquire(arg)尝试去获取锁就能成功,所以会执行if分支
            if (p == head && tryAcquire(arg)) {
                //将Thread-2的Node节点设置成头节点
                setHead(node);
                //之前的头节点都设置成null,可以被GC回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                //Thread-2阻塞在这里,被唤醒之后就会继续执行这个循环
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

所以最后Thread-2线程释放锁之后,此时链表的头节点是Thread-2的Node节点了,Thread-2线程释放锁之后,也会唤醒Thread-3线程,然后继续执行。

以上就是深度解析Java中的ReentrantLock原理的详细内容,更多关于Java ReentrantLock原理的资料请关注脚本之家其它相关文章!

相关文章

  • Java 超详细讲解IO操作字节流与字符流

    Java 超详细讲解IO操作字节流与字符流

    本章具体介绍了字节流、字符流的基本使用方法,图解穿插代码实现。 JAVA从基础开始讲,后续会讲到JAVA高级,中间会穿插面试题和项目实战,希望能给大家带来帮助
    2022-03-03
  • 解决Springboot启动报错:类文件具有错误的版本61.0,应为 52.0

    解决Springboot启动报错:类文件具有错误的版本61.0,应为 52.0

    这篇文章主要给大家介绍了关于解决Springboot启动报错:类文件具有错误的版本 61.0,应为 52.0的相关资料,这是查阅了网上的很多资料才解决的,分享给大家,需要的朋友可以参考下
    2023-01-01
  • SpringBoot详细讲解异步任务如何获取HttpServletRequest

    SpringBoot详细讲解异步任务如何获取HttpServletRequest

    在使用框架日常开发中需要在controller中进行一些异步操作减少请求时间,但是发现在使用@Anysc注解后会出现Request对象无法获取的情况,本文就此情况给出完整的解决方案
    2022-04-04
  • Java VisualVM监控远程JVM(详解)

    Java VisualVM监控远程JVM(详解)

    下面小编就为大家带来一篇Java VisualVM监控远程JVM(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Java数据结构之实现跳表

    Java数据结构之实现跳表

    今天带大家来学习Java数据结构的相关知识,文中对用Java实现跳表作了非常详细的图文解说及代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-05-05
  • Java图形用户界面之列表框

    Java图形用户界面之列表框

    列表框通过Swing组件JList产生,其总是在屏幕上占据固定行数的空间。这篇文章主要介绍了java图形用户界面之列表框的相关资料,非常不错具有参考借鉴价值,需要的朋友可以参考下
    2016-10-10
  • 详解Java多线程和IO流的应用

    详解Java多线程和IO流的应用

    这篇文章主要介绍了详解Java多线程和IO流的应用,无论是本地文件复制,还是网络多线程下载,对于流的使用都是一样的,需要的朋友可以参考下
    2023-04-04
  • Java面向对象类和对象实例详解

    Java面向对象类和对象实例详解

    面向对象乃是Java语言的核心,是程序设计的思想,这篇文章主要介绍了Java面向对象类和对象的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2022-03-03
  • Java代码审计的一些基础知识你知道吗

    Java代码审计的一些基础知识你知道吗

    这篇文章主要介绍了基于Java的代码审计功能的基础知识,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2021-09-09
  • Java 本地方法Native Method详细介绍

    Java 本地方法Native Method详细介绍

    这篇文章主要介绍了 Java 本地方法Native Method详细介绍的相关资料,需要的朋友可以参考下
    2017-02-02

最新评论