AQS源码探究_01 手写一个简化的ReentrantLock可重入锁

Posted 兴趣使然の草帽路飞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AQS源码探究_01 手写一个简化的ReentrantLock可重入锁相关的知识,希望对你有一定的参考价值。

文章参考:小刘老师的源码课

完整代码

MiniLock

/**
 * @author csp
 * @date 2021-05-01
 */
public interface MiniLock {
    /**
     * 加锁
     */
    void lock();

    /**
     * 释放锁
     */
    void unlock();
}

MiniReentrantLock

/**
 * @author csp
 * @date 2021-05-01
 * MiniReentrantLock 是一个独占锁、公平锁、可重入锁
 */
public class MiniReentrantLock implements MiniLock {
    /**
     * 加锁的状态:=0 表示未加锁状态,>0 表示加锁状态
     * 锁的是什么? -> 锁的是线程资源
     */
    private volatile int state;

    /**
     * 当前独占该锁的线程:
     * 什么是独占模式?
     * 同一时刻,只能有一个线程可以持有该锁,其他线程,在未获取到锁时,会被阻塞
     */
    private Thread exclusiveOwnerThread;

    /**
     * 维护阻塞队列需要的2个引用:
     * 1.head 指向队列头部的节点 -> head节点对应的线程,就是当前占用锁的线程!
     * 2.tail 指向队列尾部的节点
     */
    /**
     * 队列头部
     */
    private Node head;
    /**
     * 队列尾部
     */
    private Node tail;

    /**
     * 阻塞的线程被封装成Node节点,然后放入FIFO队列
     */
    static final class Node {
        // 前置节点引用
        Node prev;
        // 后置节点引用
        Node next;
        // 封装的线程
        Thread thread;

        public Node() {
        }

        public Node(Thread thread) {
            this.thread = thread;
        }

        public Node(Node prev, Node next, Thread thread) {
            this.prev = prev;
            this.next = next;
            this.thread = thread;
        }
    }

    /**
     * 获取锁:
     * 假设当前锁被占用,则会阻塞调用线程,知道抢占到锁为止
     * 模拟公平锁:公平锁讲究线程的先来后到!
     * <p>
     * lock加锁的过程:
     * 情景1:当线程执行时,当前state == 0,则该线程刚好可以直接抢到锁
     * 情景2:当线程执行时,当前state > 0,这时候就需要将当前线程入队
     */
    @Override
    public void lock() {
        // 令当前线程竞争资源
        // 第1次获取到锁时,将 state = 1
        // 第n次获取到锁时,将 state = n
        acquire(1);
    }

    /**
     * 当前线程竞争资源的方法;
     * 1.尝试获取锁,获取成功,则占用锁,并返回
     * 2.尝试获取锁,获取失败,则阻塞当前线程
     *
     * @param arg
     */
    private void acquire(int arg) {
        // 尝试获取锁失败
        if (!tryAcquire(arg)) {
            // 更复杂的逻辑来了~
            // 将当前线程添加到阻塞队列
            Node node = addWaiter();
            // 入队后,令当前线程不断去竞争资源,直到成功获取锁才停止自旋
            acquireQueued(node, arg);
        }

        // 尝试获取锁成功,则当前独占锁的线程exclusiveOwnerThread以及相关属性都在tryAcquire方法中执行了~
    }

    /**
     * 尝试抢占锁失败时,需要做些什么呢?
     * 1.将当前线程封装成node,加入到阻塞队列
     * 2.将当前线程park掉,使线程处于挂起状态,等待被唤醒
     *
     * 那么,当唤醒后需要做什么呢?
     * 1.检查当前node节点是否为 head.next节点
     *      (head.next节点是拥有抢占权限的线程,其他的node节点都没有抢占的权限)
     * 2.抢占
     *      抢占成功:则将当前node设置为head,将老的head移出队列,接着返回到业务层面
     *      抢占失败:则将当前线程继续park,等待被唤醒
     *
     * =====>
     * 1.将当前线程添加到阻塞队列的逻辑 addWaiter()
     * 2.当前线程竞争资源的逻辑       acquireQueued()
     */

    /**
     * 将当前线程添加到阻塞队列: head为持有锁的节点
     * addWaiter()执行完毕后,保证当前线程已经入队成功!
     *
     * @return 发返回当前线程对应的Node节点
     */
    private Node addWaiter() {
        // 新建当前线程node节点
        Node newNode = new Node(Thread.currentThread());

        // 如何入队呢?
        // 1.找到newNode的前置节点 pred
        // 2.更新newNode.prev = pred
        // 3.CAS更新tail为 newNode
        // 4.更新 pred.next = newNode

        // 第1种情况,前置条件:队列已经有等待者node了(不为空),当前node并不是第一个入队的node
        Node pred = tail;
        if (pred != null) {
            newNode.prev = pred;
            // 如果条件成立,说明当前线程成功入队!
            if (compareAndSetTail(pred, newNode)) {
                pred.next = newNode;
                return newNode;
            }
        }

        // 执行到这里有以下2种情况:
        // 1.tail == null 队列是空队列
        // 2.cas设置当前newNode 为 tail 时失败了,被其他线程抢先一步了
        // 自旋入队,只有入队成功才结束自旋:
        eng(newNode);

        return newNode;
    }

    /**
     * 令当前线程不断去竞争资源,直到成功获取锁才停止自旋
     *
     * @param node
     * @param arg
     */
    private void acquireQueued(Node node, int arg) {
        // 只有当前node成功获取到锁以后,才会跳出自旋:
        for (; ; ) {
            // 什么情况下,当前node被唤醒之后可以尝试获取锁呢?
            // 只有一种情况:当前node是head的后继节点,才有这个权限(FIFO队列,先来后到的道理~)
            Node pred = node.prev;
            // head节点就是当前有权去持锁的节点
            if (pred == head /* 条件成立:说明当前node拥有抢占权限 */ && tryAcquire(arg) /* 尝试获取锁 */) {
                // 当进入if里面时,说明当前线程竞争锁成功了!
                // 接下来需要做什么?
                // 1.设置当前head为当前线程的node
                // 2.协助原始head出队
                setHead(node);
                pred.next = null;// help GC
                return;
            }

            // 如果没有持锁抢占权限,则当前线程挂起,继续等待...
            // park...线程挂起
            System.out.println("线程:" + Thread.currentThread().getName() + " 挂起!");
            LockSupport.park();
            System.out.println("线程:" + Thread.currentThread().getName() + " 唤醒!");
        }
    }

    /**
     * 自旋入队的方法,只有成功后才结束自旋:
     * 1.tail == null 队列是空队列
     * 2.cas设置当前newNode 为tail 时失败了,被其他线程抢先一步了
     */
    private void eng(Node node) {
        // 自旋~
        for (; ; ) {
            // 第1种情况:空队列 ===> 即,当前线程是第一个抢占锁失败的线程
            // 当前持有锁的线程(注:tryAcquire方法直接获取到锁的线程,在该方法逻辑中,并没有将持锁线程入队,
            // 而按理说阻塞队列的head节点就应该是当前持有锁的线程才对)并没有设置过任何 node,
            // 所以作为该线程的第一个后驱next,需要给它擦屁股(给持锁线程补一个node节点并设置为阻塞队列的head
            // head节点任何时候,都代表当前占用锁的线程)
            if (tail == null) {
                // 如果compareAndSetHead条件成立:说明当前线程给当前持有锁的线程,补充head操作成功了!
                if (compareAndSetHead(new Node())) {
                    // tail = head 表示当前队列只有一个元素,这里就表名当前持锁的线程被放入阻塞队列且为head了~
                    tail = head;
                    // 注意:并没有直接返回,还会继续自旋,下次再进入循环时阻塞队列已经不为空,且head为持锁线程节点了...
                }
            } else {
                // 其他情况,说明:当前队列中已经有node了,这里是一个追加node的过程

                // 如何入队呢?和 addWaiter方法入队逻辑一样~
                // 1.找到newNode的前置节点 pred
                // 2.更新newNode.prev = pred
                // 3.CAS更新tail为 newNode
                // 4.更新 pred.next = newNode

                // 前置条件:队列已经有等待者node了(不为空),当前node并不是第一个入队的node
                Node pred = tail;
                if (pred != null) {
                    node.prev = pred;
                    // 如果条件成立,说明当前线程成功入队!
                    if (compareAndSetTail(pred, node)) {
                        pred.next = node;
                        // 注意:入队成功,一定要return结束,无限for循环~
                        return;
                    }
                }
            }
        }
    }

    /**
     * 尝试获取锁的方法,不会阻塞线程
     *
     * @param arg
     * @return true -> 尝试获取锁成功 | false -> 尝试获取锁失败
     */
    private boolean tryAcquire(int arg) {
        // 如果当前state加锁状态等于0
        if (state == 0) {
            // 条件1:!hasQueuePredecessor():表示如果当前线程前面没有等待的线程(保证公平)
            // 条件2:compareAndSetState(0,arg):使用CAS的方式对state进行修改操作执行成功
            // 如果条件1、2均成立,则说明当前线程抢锁成功!
            if (!hasQueuePredecessor() && compareAndSetState(0, arg)) {
                // 抢锁成功:
                // 1.需要把exclusiveOwnerThread 设置为当前进入if块中的线程
                this.exclusiveOwnerThread = Thread.currentThread();
                // 尝试获取锁成功
                return true;
            }
            // 如果当前线程就是持有锁的线程,且state > 0
        } else if (Thread.currentThread() == this.exclusiveOwnerThread) {
            // 该if判断中存在并发么? 不存在 -> 因为只有当前加锁的线程,才有权限修改state

            // 获取当前线程的加锁状态
            int s = getState();
            s = s + arg;

            // 越界判断...(略),在ReentrantLock中这里肯定要做越界判断的

            // 把s赋给state,更新加锁状态
            this.state = s;

            // 尝试获取锁成功
            return true;
        }
        // 尝试获取锁失败:
        // 1.CAS加锁失败 且 当前线程前面有等待的线程
        // 2.state > 0 且 当前线程不是占用锁的线程
        return false;
    }

    /**
     * 方法调用链:
     * lock -> acquire -> tryAcquire  -> hasQueuedPredecessor (ps:state值为0 时,即当前Lock属于无主状态..)
     */

    /**
     * 判断FIFO队列是否为空:
     *
     * @return true -> 表示当前线程前面有等待者线程 | false -> 表示当前线程前面没有等待者线程
     */
    private boolean hasQueuePredecessor() {
        Node h = head;
        Node t = tail;
        Node s;

        // 什么时候返回false呢?
        // 1.当前队列为空
        // 2.当前线程为head.next节点线程,head.next在任何时候都有权限去争取lock

        // 条件1:h != t
        // 成立:说明当前队列已经有node了
        // 不成立:1. h == t == null   2. h == t == head 第一个获取锁失败的线程,会为当前持有锁的线程 补充创建一个 head 节点。

        // 条件2:((s = h.next) == null || s.thread != Thread.currentThread())
        // 排除几种情况:
        // 条件2.1:(s = h.next) == null
        // 极端情况:第一个获取锁失败的线程,会为 持锁线程 补充创建 head ,然后再自旋入队,  1. cas tail() 成功了,2. pred【head】.next = node;
        // 其实想表达的就是:已经有head.next节点了,其它线程再来这时  需要返回 true
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        // 释放锁
        release(1);
    }

    /**
     * 释放锁的方法
     *
     * @param arg
     */
    private void release(int arg) {
        // tryRelease条件成立:说明线程已经完全释放锁了
        // 需要干点啥呢?
        // 阻塞队列里面还有好几个睡觉的线程呢? 是不是 应该喊醒一个线程呢?
        if (tryRelease(arg)) {
            // 获取head节点
            Node head = this.head;

            // 你得知道,有没有等待者?
            // 如果head.next == null 说明没有等待者,
            // 如果head.next != null 说明有等待者,去唤醒该等待者线程
            if (head.next != null) {
                // 公平锁,就是唤醒head.next节点
                unparkSuccessor(head);
            }
        }
    }

    /**
     * 唤醒线程节点
     *
     * @param node
     */
    private void unparkSuccessor(Node node) {
        // head之后的节点
        Node s = node.next;
        if (s != null && s.thread != null) {
            // 等待者线程唤醒
            LockSupport.unpark(s.thread);
        }
    }

    /**
     * 实际上的完全释放锁的方法
     * 完全释放锁成功,则返回true|否则,说明当前state > 0 ,返回false.
     */
    private boolean tryRelease(int arg) {
        int c = getState() - arg;

        if (getExclusiveOwnerThread() != Thread.currentThread()) {
            throw new RuntimeException("No! You must getLock!");
        }
        // 如果执行到这里?存在并发么? 只有一个线程 ExclusiveOwnerThread 会来到这里。

        // 条件成立:说明当前线程持有的lock锁 已经完全释放了..
        if (c == 0) {
            // 需要做什么呢?
            // 1.ExclusiveOwnerThread 置为null
            // 2.设置 state == 0
            this.exclusiveOwnerThread = null;
            this.state = c;
            return true;
        }

        // state不等于0,不能释放锁,返回false
        this.state = c;
        return false;
    }

    private void setHead(Node node) {
        this.head = node;
        // 为什么thread设置为null? 因为当前node已经是获取锁成功的线程了
        node.thread = null;
        node.prev = null;
    }

    public int getState() {
        return state;
    }

    public Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

    public Node getHead() {
        return head;
    }

    public Node getTail() {
        return tail;
    }

    /**
     * 基于CAS,得到state、head、tail属性的setter方法
     */
    private static final Unsafe unsafe;
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);

            stateOffset = unsafe.objectFieldOffset
                    (MiniReentrantLock.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                    (MiniReentrantLock.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                    (MiniReentrantLock.class.getDeclaredField("tail"));

        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

以上是关于AQS源码探究_01 手写一个简化的ReentrantLock可重入锁的主要内容,如果未能解决你的问题,请参考以下文章

Java小白进阶系列——Java锁框架AQS源码分析目录大纲

Java小白进阶系列——Java锁框架AQS源码分析目录大纲

AQS源码探究_05 Conditon条件队列(手写一个入门的BrokingQueue)

AQS源码探究_05 Conditon条件队列(手写一个入门的BrokingQueue)

AQS源码探究_02 AQS简介及属性分析

AQS源码探究_02 AQS简介及属性分析