【ReentrantLock】底层实现

ReentrantLock

synchronized 倒序唤醒 EntryList JVM 底层C++实现

ReentrantLock 顺序唤醒 Java实现

都是双向链表

上锁

private volatile int state; //锁状态,加锁成功则为1,重入+1 解锁则为0

非公平锁 直接尝试能不能设置状态,可以就直接占用,否则就等待锁

java 复制代码
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

公平锁 直接去等待

java 复制代码
    final void lock() {
        acquire(1);
    }

acquire上锁方法

先去tryAcquire(arg)尝试获得锁。要还是获取不了,就将自己加入队列去排队acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

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

tryAcquire尝试加锁

在本文中,this都是ReentrantLock这把锁

先是获取当前线程,然后查看ReentrantLockstate是不是等于0,等于0就代表着,没有被上锁,可以去尝试地加锁。尝试地过程中,肯定有可能抢不过别人,被人把锁给抢走了。

java 复制代码
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

要注意这个hasQueuedPredecessors方法放回的是要取反的,所以接下来我们要进入到这个方法查看。

hasQueuedPredecessors 需不需要排队

java 复制代码
    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());
    }

h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); 都只考虑返回false

这条语句有两个判断,先看第一个

java 复制代码
//t是双向链表的尾部,h是头部
先考虑为false,false的话就可以不用看后面的语句了
h == t (h != t)返回的就是false了
1.双向链表未初始化,都是null值,代表着当前线程是第一个进来的,不用排队等解锁,直接去上锁(在这种情况下,有可能是这样子的,两个线程都觉得自己是第一个,从而都是cas,但这样,只会有一个成功,另外那个线程就变成了第二个线程了)

只有上面一种情况h != t才返回false

因为这个双向链表的头节点都是 null后面再探究这个,这里先说明一下

((s = h.next) == null || s.thread != Thread.currentThread()) 来看一下这条语句什么时候返回false

java 复制代码
(s = h.next) == null //返回false需要s != null 很容易理解,就是双向链表里有一个数据

因为是||运算,所以还要看后面的语句

java 复制代码
s.thread != Thread.currentThread() // 这个也很容易理解,就是当前的线程等于s的线程就返回false了。

要想两个语句都返回false,等价于s这个Node类的线程就是当前线程且他是头节点的下一个节点(说白了就是第一个结点,因为头节点是NULL),所以不需要排队。

回到tryAcquire这个方法里来

java 复制代码
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                //经过上面的步骤后,false取反的true,就会进行cas操作。上面也有讲了,有可能很多个线程都觉得自己是一个线程,同时抢,cas就能保证,只有一条线程能抢到。
                compareAndSetState(0, acquires)) {
                //设置独有线程
                setExclusiveOwnerThread(current);
                //返回true代表上锁成功,不用使线程进入等待
                return true;
            }
        }
        //判断当前线程是不是上面设置的线程,是的话,继续复用这把锁,这里也说明了,ReentrantLock是一把复用(重入)锁
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        //到这里就得乖乖去排队了撒
        return false;
    }
    }

addWaiter

这个方法就是将当前线程封装成一个Node对象,然后加到ReetrantLock的双向链表中。

至于Node.EXCLUSIVE这个参数还是不太懂。

java 复制代码
    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;
            //要是能直接替换,就进行替换,不行就到enq这个方法去操作
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

---------------------
    //这个方法除了将node加到链表的末尾外,还有一个初始化链表的作用,也就是在这里,会创建一个线程为NULL的Node对象
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //初始化
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在进行上面的操作后,我们就要看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法了,要是这个方法返回true的话,我们这个当前线程就要进入到WAIT等待状态了,等待上一个线程将锁释放。

acquireQueued

这个方法主要做了什么事情呢

  1. 先获得当前node的父节点,上面我们也说了,这个ReentrantLock的锁是按照队列的方式的,顺序唤醒 的,所以我们先考虑这样子的一种情况。比如他有5个线程,都想抢占这把锁,然后此时,锁已经被第一个线程给抢占了。所以,往后的线程都得排队等待锁得释放,在上面的addWaiter方法就把不能获得锁的线程给入链表了。

  2. 在入链表后,还是会执行这个acquireQueued方法来真正对不能获得锁的线程使它进入等待状态,当第一个线程释放锁后,就会按顺序唤醒下一个链表中的线程。

  3. 使线程进入等待之前,还是会自旋两次。查看持有锁的线程是否已经释放锁,释放了就接着尝试获得锁,要是没有释放,就使自己进入等待状态

    ReentrantLock

    UNSAFE.park() 使线程进入等待状态

    UNSAFE.unpark() 使线程在等待状态的位置醒来

java 复制代码
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环,在第一个if循环两次尚未获得锁后,将使自身进入等待状态
            for (;;) {
            	// 获取当前线程Node对象的父节点
                final Node p = node.predecessor();
                /*
                * 当父节点是整个链表的头结点时,就说明,当前线程是位于第二位的,第一位正在占有锁,这个时候,我们就可以去尝试获得锁,万一成功了呢,对吧。
                * 但是,父节点都不是头结点了,当前线程也就没有资格去获取锁了,因为当前线程前面还有线程在等着抢占锁呢。
                * 当当前线程是第二位是,他就又会tryAcquire尝试获得锁,在获得锁后,把自己设置为头结点,然后清空自己Node对象的线程,和父节点。
                * 将自己父节点的子节点给置空,此时,自己便成为了链表中的第一个节点,节点中的线程是NULL,这个也对应了上面所说的,链表中的第一个元素的线程一定为NULL!
                */
                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);
        }
    }

shouldParkAfterFailedAcquire 获取锁失败后,应该进入等待

上面讲了,acquireQueued执行时,会自旋两次,在自旋两次的时候,干了什么事情,怎么控制自旋两次的,就都是这个方法进行操控的。

  • 第一遍自旋时,获取不了锁,就会进入到这个方法里。首先,先获取父节点的等待状态(节点的状态)。
  • 这个值肯定为0,因为从一开始到现在,我们都没有对waitStatus进行过操作,那么它肯定是为0的,然后下面的逻辑就会将父节点的waitStatus设置为-1,代表着整个线程进入了睡眠(这里有个疑问,头节点的waitStatus的值为-1除了给子节点当一个标识符之外,就好像没有了什么意义了啊,除了父节点外的都可以代表着,当前Node进入了等待状态。不懂 在解锁的时候发现了,当头节点的waitStatus != 0时,就会唤醒下一个等待的线程, 懂了!!!!)
  • 第一遍设置为-1后,返回的值是false,就不会进入到parkAndCheckInterrupt使线程等待了。
  • 然后进行第二遍自旋,发现还是获取不了锁,就重新进入这个方法。现在,父节点的的waitStatus就是-1了,然后就会返回true,就会进入parkAndCheckInterrupt方法,调用UNSAFE使线程等待。
java 复制代码
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 第一遍进入时,为0, 第二遍进入时,上一遍改变了,现在为-1
        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;
            // waitStatus 为0 时,将状态修改为-1,并返回false
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt 使线程进入等待

这个方法较为简单,就是调用LockSupport.park(this)让线程进入等待

java 复制代码
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

总结

至此,线程的获取锁也讲的差不多了,在这回顾一下。

首先,一个线程想获得ReentrantLock的锁,有几个方法,首先是直接去尝试获取,当ReentrantLock中的链表尚未初始化时,就直接对锁的status进行CAS修改,修改成功,那么就成功获得锁。若是有多个线程 同时都发现链表没有初始话,同时CAS,就会有一个线程无法抢到锁,进行链表的初始化

链表的初始化,先实例化一个线程为NULL的Node作为头节点,然后将需要排队的线程封装为一个新的Node对象,排在头节点的后面。两次自旋后,将自身等待。

抢占锁的线程释放锁后,就换唤醒排队的下一个线程,唤醒的线程会在进入等待的代码中醒来,接着执行代码。然后醒来的线程,继续死循环,发现,可以获得锁了,然后就执行这个线程自己的逻辑。

解锁

解锁的过程相较于上锁的过程较为简单,主要就是因为,没有获得锁的线程都进入了等待状态,不用设计这么多的逻辑预处理。

解锁的步骤分为两步。

  1. 改变锁的状态值
  2. 唤醒下一个等待的线程
java 复制代码
    public void unlock() {
        sync.release(1);
    }

release 解锁方法

这个方法,先是去尝试解锁,解锁成功后,查看有没有线程在排队,并且头节点的状态值不等于0时(上面讲过,父节点的状态值是由子节点在等待前进行修改的),就会唤醒下一个等待的线程。

java 复制代码
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为NULL并且状态值!= 0,就去唤醒下一个线程。
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease

方法较为简单,不再描述

java 复制代码
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

unparkSuccessor 唤醒下一个线程

这里我们着重讲解一下,这个方法究竟干了什么事情。

传入的node是链表的头节点

  1. 获取头节点的状态值,小于0变更为0。
  2. 获取头节点的子节点。
  3. 当子节点不为空时,调用UNSAFE唤醒子节点的线程。
java 复制代码
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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)
            LockSupport.unpark(s.thread);
    }

然后子节点的线程被唤醒后,就会去抢占锁,一个线程的加锁、解锁到这里也就讲完了。

在ReentrantLock中还有着许多的API来讲解的,这次就单单讲述这两个方法吧。

相关推荐
考虑考虑2 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261352 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊3 小时前
Java学习第22天 - 云原生与容器化
java
渣哥5 小时前
原来 Java 里线程安全集合有这么多种
java
间彧5 小时前
Spring Boot集成Spring Security完整指南
java
间彧5 小时前
Spring Secutiy基本原理及工作流程
java
Java水解6 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆8 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学9 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole9 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端