ReentrantLock底层原理以及AQS详解

前言:

        本文章内容较多,请耐心看完,看完之后能学到的东西有:可以自己能实现一个锁,对CAS的理解,ReentrantLock底层原理,对线程 "中断" 的理解。

1.自己实现一个锁

  • 在说ReentrantLock底层原理之前,我们先用CAS来实现一个非公平锁,这样有利于我们了解底层原理

public class test {

    public static void main(String[] args) {

        ZDY zdy = new ZDY();

        new Thread(new Runnable() {
            @Override
            public void run() {
                zdy.取钱();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                zdy.取钱();
            }
        }).start();
    }
}

class ZDY {
    public void 取钱()
    {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程" + Thread.currentThread().getName() + "正在取钱");
        }
    }
}

运行结果:

920bb23c0fc9458cae10d4fd1a7996c6.png
 

可以看到,现在已经出现线程问题了,现在需要自己来实现一个锁,来保证有序输出

  • 自己使用CAS来实现非公平锁

        在说CAS之前,需要说一下多线程的主内存以及工作内存之间的关系:多线程之间的共享变量是存在主内存的,而每一个线程都会有工作内存,这个工作内存是抽象出来的,不是真实存在的,每一个线程都会读取主内存中的共享数据以副本的形式保存到自己的工作内存中,然后对自己工作内存中的数据进行处理,但是这个处理的结果其他线程是看不到的,只有当前线程可以看得到,这是造成线程不可见性的主要原因。

        CAS原理:有三个参数,旧的期望值A,要更新的值B,还有一个当前主内存的值V,A值是第一次读取主内存中的值,而V则是要把数值B更新到主内存前读取到的值,当A等于V的时候,才会进行一个更新,要不然就重试,这就是CAS的基本原理

        volatile:可以保证可见性,被volatile修饰的共享变量,只要它在某一个线程中的值被改变了,那么就会把改变后的值强制性的写回主内存,并使其他线程中的这个共享变量失效
当其他线程使用这个共享变量时候就会发现失效,那么就会再次往主内存中读取,就这保证了其他线程读取到的这个共享变量的值一直都是最新的,它还可以禁止重排序优化,但是不能保证原子性
        粗略的理解以上三个知识点之后,就可以来动手实现了

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class test {

    public static void main(String[] args) {

        ZDY zdy = new ZDY();

        new Thread(new Runnable() {
            @Override
            public void run() {
                zdy.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                zdy.取钱();
                zdy.unlock();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                zdy.lock();
                zdy.取钱();
                zdy.unlock();
            }
        }).start();
    }
}

class ZDY {

    private static  Unsafe UNSAFE = null;

    private volatile int state;

    // state状态值的一个偏移量
    private static long stateOffset;

    static {
        try {

            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            UNSAFE = (Unsafe) f.get(null);

            stateOffset = UNSAFE.objectFieldOffset(ZDY.class.getDeclaredField("state"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void lock(){

        /*
         compareAndSwapInt:这个方法就是cas的具体实现,这个方法是要对this中的state进行修改(偏移量是stateOffset),
         在修改之前,state的值必须是0(对应方法的第三个参数),把这个值改成1
         */
        while (!UNSAFE.compareAndSwapInt(this,stateOffset,0,1))
        {

        }

    }

    public void unlock(){
        this.state = 0;
    }

    public void 取钱()
    {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程" + Thread.currentThread().getName() + "正在取钱");
        }
    }

}

通过运行代码就可以看到已经实现了锁的机制,主要是:while (!UNSAFE.compareAndSwapInt(this,stateOffset,0,1)),这行代码,假如两个线程同时执行到 lock 方法中,其中线程A先执行了这个代码,把0改成了1,那么就表示获取到了锁,那么另外一个线程B执行这方法就会失败,返回false,通过取反就会一直循环等待,直到线程A执行了unlock方法,把1变成了0,那么线程B就可以修改成功,往下执行。

  • park方法的用处

import java.util.concurrent.locks.LockSupport;

public class parkTest {
    public static void main(String[] args) throws InterruptedException {

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程A执行park方法前");
                LockSupport.park();
                System.out.println("线程A继续执行");
            }
        });
        threadA.start();

        Thread.sleep(1000);

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程B执行unpark方法前");
                LockSupport.unpark(threadA);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程B继续执行");
            }
        });
        threadB.start();


    }
}

通过运行代码就可以知道,线程A执行 LockSupport.park 方法后被暂停了,等待线程B执行 LockSupport.unpark(threadA) 方法后就可以重新执行,使用park方法就可以实现一个公平锁

搞懂以上代码,那么ReentrantLock的底层原理就明白了30%了

2.ReentrantLock底层原理

bb28605a51544b258d3a13c7d18f111b.png

可以看到,lock方法有两个实现,FairSync 是公平锁,NonfairSync 是非公平锁,可以在创建ReentrantLock对象的时候,传递 true 参数就表示创建公平锁,否则就默认创建非公平锁

  • 公平锁

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

非公平锁的 lock 方法底层有三个方法:tryAcquire,acquireQueued,addWaiter
首先 tryAcquire 方法是用来获取锁的,如果获取成功,则会返回true,然后通过取反,就变成了false,就不会执行后面的两个方法。

  • tryAcquire

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;
        }

        首先线程会进入这个方法来尝试获取锁,首先获取当前线程,然后获取 state 的值,这个值实际上跟我们自己实现锁的案例定义的值是一模一样的。

        然后进行比较,假如 c 的值等于0,则表示这个锁暂时还没有被线程所持有,可以尝试来获取锁,因为公平锁是有维护一个队列的(先到先得,在队列中最前面的线程是可以最先获取锁的),所以 hasQueuedPredecessors 方法就是用来判断当前线程是否为队列中的最前面的那一个,如果返回false,则表示前面是没有线程进行等待,当前线程就是队列中的最前面的线程,然后通过取反变成 true,就可以通过 cas 来改变 state 的值,如果修改成功则表示获取锁成功,则继续往下执行,通过 setExclusiveOwnerThread 方法来设置持有锁的线程为当前线程。          

        如果 的值不等于0,则要进行判断,因为 ReentrantLock 是一个可重入锁,如果一个已经获取到锁的线程,重复调用 lock 方法,会经过:获取到锁的线程等于当前线程 的这个判断,会重复执行 int nextc = c + acquires 这个代码,让 state 的值加1,这就是可重入锁的关键代码。
        假如有两个线程来同时执行 非公平锁 lock 方法,最开始的时候 stat 的值为 0,假如两个线程同时执行:hasQueuedPredecessors 方法,那这两个线程的对于这个方法的返回肯定是false,但是通过 cas 方法执行后,肯定会有一个线程得到的返回值是false(意味着没有得到锁),得到 false 返回值的这个线程就会进入到下一个步骤:排队

        hasQueuedPredecessors方法在下面会讲解!

  • addWaiter

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;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

        竞争锁失败的线程会进入到这个方法,假如有线程B,线程C进入到 addWaiter 方法中,这个方法主要是给队列添加节点,假如线程B先进入这个方法(在实际开发中如果线程特别多的情况下,是有可能很多个线程一起进入这个方法的),这个时候队列的头结点和尾结点都为null,那么就不会走 if (pred != null) 里面的代码,而是直接执行 enq 方法:

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;
                }
            }
        }
    }

        在 enq 方法中,是先获取到尾结点,因为尾结点为 null,所以就会使用 cas 来设置头结点(之所以要用 cas,那是因为如果很多个线程都进入这个 enq 方法中,但是呢这个时候的尾结点为 null,那就要涉及到线程之间的资源竞争,那就得使用 cas),设置好头节点之后,就会继续循环进入到 else 分支中,在 else 分支中会有并发现象(例如进入 enq 方法的有A,B,C线程,那第一次进入 else 分支中,这三个线程的前节点都要指向尾结点,但是通过cas 来竞争尾结点可以修正这个情况),这个 addWaiter 方法返回的是最后一个节点的 node 对象,然后就进入到:acquireQueued 方法中

  • acquireQueued

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);
        }
    }

        通过 addWaiter 添加的 node 对象会进入到这个方法中,会调用 node.predecessor 获取到当前节点的前一个节点,假如当前节点的前一个节点 等于 head 节点,那就说明当前线程是在队列里面的最前面的线程,那就通过 tryAcquire 方法获尝获取锁,假如持有锁的线程还没释放锁,那么就算当前线程是最先排队的,一样获取锁失败。

        假如获取锁成功,会执行setHead方法

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

        可以看到在 setHead 方法中 ,会把头节点指向新的 node 节点,然后新的头节点的当前线程属性为 null ,新的头节点前一个节点设为 null,然后旧的头节点的下一个节点为 null,通过这个操作就可以把原来的头节点给删除掉(在这个队列中,实际上是第二个的节点才是队列里面最靠前的),然后返回 interrupted 这个中断标志位。如果中断标志位的值是 false的话,那:

if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();

这一行代码整体得到的值就为 false,就不会执行 selfinterupt 方法。
        如果第一次执行 if (p == head && tryAcquire(arg)) 判断,返回的是 false(之所以返回false,有可能是当前节点的前一个节点不是头节点,或者有别的线程还没释放锁)那就会继续往下执行,执行 shouldParkAfterFailedAcquire 方法

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 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;
    }

        首先,第一次执行 shouldParkAfterFailedAcquire  方法的 node 对象的前一个节点的 waitstatus 值为 0(这个 0 是默认的,除非前一个节点被"中断"了,如果是被中断了,那前一个节点的 waitstatus 值是为 1 的),然后进行判断,因为 Node.SIGNAL 的值是为 -1 的,那么第一次判断肯定不相等,然后就会通过 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 方法,把当前节点的前一个节点的 ws 值改成 -1,然后返回 false,那这个时候我们进行回到 acquireQueued 方法中

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);
        }
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

        当第一次执行 shouldParkAfterFailedAcquire 方法后,返回 false,通过循环继续来执行循环体里面的方法,如果 if (p == head && tryAcquire(arg)) 继续返回的是 false,那就会再一次进入到 shouldParkAfterFailedAcquire 方法里面,这个时候当前节点的前一个节点的 waitstatus 值是为 -1,那么就会返回 true,就会继续执行 parkAndCheckInterrupt 方法,在 parkAndCheckInterrupt 方法里面会先执行 LockSupport.park 方法,使得当前线程停止运行。

        当这个线程被其他线程 unpark 的时候,就会解除阻塞,然后调用 Thread.interrupted 方法来获取当前线程在被 park 的期间,是否有被中断,可以通过 Thread.interrupted 来获取返回值,并清除中断标志位,通过 interrupted 方法的返回值,来判断是否要执行下面的 interrupted = true 代码。

        如果执行了 interrupted = true 那就说明当前线程在执行 park 期间,是有被中断的,但是因为执行了 Thread.interrupted 方法,就算当前线程是被其他线程中断了,也会被 interrupted 这个方法把当前线程的中断标志位改成了 false。

        如果 acquireQueued 返回的是 true,说明当前线程已经被中断了,所以就会进入selfInterrupt 方法中,把当前线程的状态重新设置回中断的状态,不能改变用户对于该线程的操作,用户想改变它的中断标志,AQS 不能擅自改变用户操作。

        这里有很多地方涉及到对线程的中断知识点,对中断不理解或者有疑惑的,可以看下我写的这篇文章:

        线程中断原理详解_流连勿忘返的博客-CSDN博客

  • hasQueuedPredecessors

在理解以上代码之后,就来看这个方法

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());
    }

        hasQueuedPredecessors 方法是用来判断当前线程是否为队列中最前面的线程

        在这里有头节点跟尾结点,首先要判断头结点是否不等于尾结点,如果不明白头节点不等于尾结点代表什么意思,那我们可以想一下头节点等于尾结点的情况。        

        头节点等于尾结点不就是代表了队列里面没有元素吗,所以当队列里面没有元素的时候,头节点等于尾结点,那么 h != t 就返回 false,表示的就是当前线程的前面没有正在排队的线程,当前线程可以去尝试获取锁。

        如果 h != t 返回的是 true,则表示这个队列里面是有元素的,就会执行下面的 ((s = h.next) == null || s.thread != Thread.currentThread()) 代码。

        先不看 (s = h.next) == null 这个,而是先看 s.thread != Thread.currentThread() ,因为是先执行了 s = h.next 代码,所以 s 就代表了 头结点的下一个节点,然后通过 s.thread != Thread.currentThread() 来做比较,如果经过 if 判断得到头结点的下一个节点 (s.thread 代表的是:节点本身的地址引用)不等于当前节点,返回了false,则表示头结点的下一个节点就是当前节点,所以整个方法的返回值就返回 false,表示当前线程的前面没有正在排队的线程,当前线程可以去尝试获取锁。

        然后我们来看下 (s = h.next) == null 的这个情况,这行代码是这个方法的重点!,我第一次看这行代码的时候,我还以为说的是头节点的下一个节点为 null,但是真的有可能出现头结点不等于尾结点,然后头结点的下一个节点为 null 吗?其实这个里考虑的是一个并发的情况,我们先来梳理一下整个流程:

        

       假如线程A获取到锁,然后线程B也想来获取锁,那就会进入 tryAcquire 方法,先执行tryAcquire 方法中的 hasQueuedPredecessors 方法来判断队列是否有排队元素,这个时候头结点跟尾结点都不存在,肯定是返回 false,通过取反来执行 compareAndSetState 方法,因为线程A现在还没释放锁,所以就获取锁失败,然后就会执行 addWaiter 方法,然后在 addWaiter 方法中的 enq 方法里面构建好了头结点,尾节点

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;
                }
            }
        }
    }

        可以看到是先进入 if (t == null) 这个判断,创建好了一个空的 node 对象,通过 cas 把空的 node 节点赋值成为头节点,然后 tail = head 赋值,这个时候的 tail head 虽然不为null,但是这个时候他们两个还是相等的。
        然后线程B会执行 node.prev = t 就是让线程B的 node 节点的前一个节点为头结点,然后通过 compareAndSetTail(t, node)  来把尾结点 tail 设置成最新的 node 节点,但是这些操作都不是原子性的,就在执行 t.next = node(把头结点下一个节点设置为线程B的 node 节点)的时候,线程C进来了。

        线程C执行 hasQueuedPredecessors 的时候,头节点不等于尾结点,但是 (s = h.next) == null 确实是存在的,因为这个时候头节点的下一个节点还没成功赋值,但是对于线程C来说就是就是要排队的,所以线程C返回 true,进行排队

  • unlock方法

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

unlock 方法底层:在 release 方法中会先进入 tryRelease 方法,现在我们先看 tryRelease 方法:

 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;
        }

        先获取 state 的值,然后减一,再判断当前线程是否等于持有锁的线程,如果不是那就直接异常,如果当前线程等于持有锁的线程,这个时候就要看 state 的值是否等于 0(因为ReentrantLock 是可重入锁,有可能代码执行了多次 lock 方法,那这里就要去判断 state 的值)

        如果 stat 的值不等于0,那就通过 setState 方法来修改,并且返回 false。

        如果 stat 的值等于0,那就把 ReentrantLock 中持有锁的线程设置为 null,并且返回ture。
        返回 true 之后就会进入到 release 方法中的 h != null && h.waitStatus != 0 这个判断中: h != null 这个无需多言,这个是肯定的。

        但是 h.waitStatus != 0 这个不太好理解:这个意思是当前的节点保存了是否要 unpark 下一个节点的标志,当前节点在释放锁之后要 unpark 下一个节点,是通过 waitStatus 标志来做判断的。

        举个例子:A在排队,然后B过来也排队了,这个时候B会拍一下A的肩膀并留下一个标志,然后说:A兄弟,我先睡眠一会,你等一下记得叫醒我,然后A释放锁的时候,发现自己身上的标志位是 -1 ,那就说明要 unpark 下一个节点。那假如B在排队的过程中,被中断了呢?如果在排队的过程中B被中断了,那B的标志位就变成了1,然后A在 unpark B节点的时候,会判断B节点的 waitStatus 的值,如果为 1,那就会从尾节点的后面开始倒着查waitStatus = -1 的节点,然后 unpark 查询到的节点

unparkSuccessor 方法

   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);
    }

        这个方法主要是关注 for (Node t = tail; t != null && t != node; t = t.prev) 这一行代码的

t != null && t != node(从尾节点的后面开始倒着查 waitStatus = -1 的最靠前的那个节点,假如:A->B->C->D->E,假如B,D被中断了,那么通过这段代码,最终得到的节点是C节点)在本方法中有个 unpark 用于释放即将要获取锁的节点

非公平锁模式

直接上代码:

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        可以看到,非公平锁一进来就会直接获取锁,如果没有获取成功,那接下来获取锁的方式就跟公平锁的方式一模一样了。

        非公平锁模式下的解锁跟上面的解锁是一样的,无论是公平锁还是非公平锁,用的都是同一个解锁方法

AQS

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

只要把 tryAcquire,acquireQueued,addWaiter 这三个方法全部掌握,那就搞懂了AQS了

以上就是全部的内容了,看不懂的可以重复多次观看,如果对你有帮助,可以点个赞。

其中本文章涉及到的中断知识点: 线程中断原理详解_流连勿忘返的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/qq_26112725/article/details/131363996