乐趣区

关于java:面经手册-第17篇码农会锁ReentrantLock之AQS原理分析和实践使用

作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!????

一、前言

如果你置信你做什么都能成,你会自信的多!

千万不要总自我否定,尤其是职场的打工人。如果你常常感觉,这个做不好,那个学不会,别的也不懂,那么长此以往会越来越缺乏自信。

个别说能成事的人都具备 赌徒 精力,在他们眼里只有做这事那就肯定能成,当然也有可能最初就没成,但在整个过程中人的心态是良好的,每天都有一个丰满的精神状态,手不释卷的奋斗着。最初也就是这样的斗志让走在一个终点的小伙伴,有了差距。

二、面试题

谢飞机,小记,明天打工人呀,今天早上困呀,嘟嘟嘟,喂?谁呀,打农药呢!?

谢飞机:哎呦,面试官大哥,咋了!

面试官:偷偷通知你哈,你一面过了。

谢飞机:嘿嘿,真的呀!太好了!哈哈哈,那我还筹备点什么呢!?

面试官:二面会比拟难喽,嗯,我顺便问你一个哈。AQS 你理解吗,ReentrantLock 获取锁的过程是什么样的?什么是 CAS?…

谢飞机 :我我我, 脑子还在后羿射箭里,我一会就看看!!

面试官:好好筹备下吧,打工人,打工魂!

三、ReentrantLock 和 AQS

1. ReentrantLock 常识链

ReentrantLock 可重入独占锁波及的知识点较多,为了更好的学习这些常识,在上一章节先剖析源码和学习实现了偏心锁的几种计划。包含:CLH、MCS、Ticket,通过这部分内容的学习,再来了解 ReentrantLock 中对于 CLH 的变体实现和相应的利用就比拟容易了。

接下来沿着 ReentrantLock 的常识链,持续剖析 AQS 独占锁的相干知识点,如图 17-1

在这部分常识学习中,会次要围绕 ReentrantLock 中对于 AQS 的应用进行开展,逐渐剖析源码理解原理。

AQS 是 AbstractQueuedSynchronizer 的缩写,简直所有 Lock 都是基于 AQS 来实现了,其底层大量应用 CAS 提供乐观锁服务,在抵触时采纳自旋形式进行重试,以此实现轻量级和高效的获取锁。

另外 AbstractQueuedSynchronizer 是一个抽象类,但并没有定义相应的形象办法,而是提供了能够被字类继承时笼罩的 protected 的办法,这样就能够十分不便的反对继承类的应用。

2. 写一个简略的 AQS 同步类

在学习 ReentrantLock 中利用的 AQS 之前,先实现一个简略的同步类,来领会下 AQS 的作用。

2.1 代码实现

public class SyncLock {

    private final Sync sync;

    public SyncLock() {sync = new Sync();
    }

    public void lock() {sync.acquire(1);
    }

    public void unlock() {sync.release(1);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {return compareAndSetState(0, 1);
        }

        @Override
        protected boolean tryRelease(int arg) {setState(0);
            return true;
        }

        // 该线程是否正在独占资源,只有用到 Condition 才须要去实现
        @Override
        protected boolean isHeldExclusively() {return getState() == 1;
        }
    }

}

这个实现的过程属于 ReentrantLock 简版,次要包含如下内容:

  1. Sync 类继承 AbstractQueuedSynchronizer,并重写办法:tryAcquire、tryRelease、isHeldExclusively。
  2. 这三个办法根本是必须重写的,如果不重写在应用的时候就会抛异样 UnsupportedOperationException
  3. 重写的过程也比较简单,次要是应用 AQS 提供的 CAS 办法。以预期值为 0,写入更新值 1,写入胜利则获取锁胜利。其实这个过程就是对 state 应用 unsafe 本地办法,传递偏移量 stateOffset 等参数,进行值替换操作。unsafe.compareAndSwapInt(this, stateOffset, expect, update)
  4. 最初提供 lock、unlock 两个办法,理论的类中会实现 Lock 接口中的相应办法,这里为了简化间接自定义这样两个办法。

2.2 单元测试

@Test
public void test_SyncLock() throws InterruptedException {final SyncLock lock = new SyncLock();
    for (int i = 0; i < 10; i++) {Thread.sleep(200);
        new Thread(new TestLock(lock), String.valueOf(i)).start();}
    Thread.sleep(100000);
}

static class TestLock implements Runnable {
    private SyncLock lock;
    public TestLock(SyncLock lock) throws InterruptedException {this.lock = lock;}
    @Override
    public void run() {
        try {lock.lock();
            Thread.sleep(1000);
            System.out.println(String.format("Thread %s Completed", Thread.currentThread().getName()));
        } catch (Exception e) {e.printStackTrace();
        } finally {lock.unlock();
        }
    }
}
  • 以上这个单元测试和咱们在上一章节介绍偏心锁时是一样的,验证程序输入。当然你也能够抉择多线程操作一个办法进行加和运算。
  • 在测试的过程中能够尝试把加锁代码正文掉,进行比对。如果能够程序输入,那么就是预期后果。

测试后果

Thread 0 Completed
Thread 1 Completed
Thread 2 Completed
Thread 3 Completed
Thread 4 Completed
Thread 5 Completed
Thread 6 Completed
Thread 7 Completed
Thread 8 Completed
Thread 9 Completed
  • 从测试后果看,以上 AQS 实现的同步类,满足预期成果。
  • 有了这段代码的概念构造,接下来在剖析 ReentrantLock 中的 AQS 应用就有肯定的感觉了!

3. CAS 介绍

CAS 是 compareAndSet 的缩写,它的利用场景就是对一个变量进行值变更,在变更时会传入两个参数:一个是预期值、另外一个是更新值。如果被更新的变量预期值与传入值统一,则能够变更。

CAS 的具体操作应用到了 unsafe 类,底层用到了本地办法 unsafe.compareAndSwapInt 比拟替换办法。

CAS 是一种无锁算法,这种操作是 CPU 指令集操作,只有一步原子操作,速度十分快。而且 CAS 防止了申请操作系统来裁定锁问题,间接由 CPU 搞定,但也不是没有开销,比方 Cache Miss,感兴趣的小伙伴能够自行理解 CPU 硬件相干常识。

4. AQS 外围源码剖析

4.1 获取锁流程图

图 17-2 就是整个 ReentrantLock 中获取锁的外围流程,包含非偏心锁和偏心锁的一些穿插流程。接下来咱们就以此依照此流程来解说相应的源码局部。

4.2 lock

ReentrantLock 实现了非偏心锁和偏心锁,所以在调用 lock.lock(); 时,会有不同的实现类:

  1. 非偏心锁,会间接应用 CAS 进行抢占,批改变量 state 值。如果胜利则间接把本人的线程设置到 exclusiveOwnerThread,也就是取得锁胜利。不胜利后续剖析
  2. 偏心锁,则不会进行抢占,而是规规矩矩的进行排队。老实人

4.3 compareAndSetState

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

在非偏心锁的实现类里,获取锁的过程,有这样一段 CAS 操作的代码。compareAndSetState 赋值胜利则获取锁。那么 CAS 这外面做了什么操作?

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

往下翻咱们看到这样一段代码,这里是 unsafe 性能类的应用,两个参数到这里变成四个参数。多了 this、stateOffset。this 是对象自身,那么 stateOffset 是什么?

stateOffset = unsafe.objectFieldOffset
    (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

再往下看咱们找到,stateOffset 是偏移量值,偏移量是一个固定的值。接下来咱们就看看这个值到底是多少!

援用 POM jol-cli

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-cli</artifactId>
    <version>0.14</version>
</dependency>

单元测试

@Test
public void test_stateOffset() throws Exception {Unsafe unsafe = getUnsafeInstance();
    long state = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
    System.out.println(state);
}

// 16
  • 通过 getUnsafeInstance 办法获取 Unsafe,这是一个固定的办法。
  • 在获取 AQS 类中的属性字段 state 的偏移量,16。
  • 除了这个属性外你还能够拿到:headOffset、tailOffset、waitStatusOffset、nextOffset,的值,最终自旋来变更这些变量的值。

4.4 (AQS)acquire

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

整个这块代码外面蕴含了四个办法的调用,如下:

  1. tryAcquire,别离由继承 AQS 的偏心锁(FairSync)、非偏心锁(NonfairSync)实现。
  2. addWaiter,该办法是 AQS 的公有办法,主要用途是办法 tryAcquire 返回 false 当前,也就是获取锁失败当前,把以后申请锁的线程增加到队列中,并返回 Node 节点。
  3. acquireQueued,负责把 addWaiter 返回的 Node 节点增加到队列结尾,并会执行获取锁操作以及判断是否把以后线程挂起。
  4. selfInterrupt,是 AQS 中的 Thread.currentThread().interrupt() 办法调用,它的次要作用是在执行完 acquire 之前本人执行中断操作。

4.5 tryAcquire

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

这部分获取锁的逻辑比较简单,次要包含两局部:

  1. 如果 c == 0,锁没有被占用,尝试应用 CAS 形式获取锁,并返回 true。
  2. 如果 current == getExclusiveOwnerThread(),也就是以后线程持有锁,则须要调用 setState 进行锁重入操作。setState 不须要加锁,因为是在本人的以后线程下。
  3. 最初如果两种都不满足????,则返回 false。

4.6 addWaiter

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果队列不为空, 应用 CAS 形式将以后节点设为尾节点
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 队列为空、CAS 失败,将节点插入队列
    enq(node);
    return node;
}
  • 当执行办法 addWaiter,那么就是 !tryAcquire = true,也就是 tryAcquire 获取锁失败了。
  • 接下来就是把以后线程封装到 Node 节点中,退出到 FIFO 队列中。因为先进先出,所以起初的队列退出到队尾
  • compareAndSetTail 不肯定肯定胜利,因为在并发场景下,可能会呈现操作失败。那么失败后,则须要调用 enq 办法,该办法会自旋操作,把节点入队列。

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;
            }
        }
    }
}
  • 自旋转for 循环 + CAS 入队列。
  • 当队列为空时,则会新创建一个节点,把尾节点指向头节点,而后持续循环。
  • 第二次循环时,则会把以后线程的节点增加到队尾。head 节是一个无用节点,这和咱们做 CLH 实现时相似

留神,从尾节点逆向遍历

  1. 首先这里的节点连贯操作并不是原子,也就是说在多线程并发的状况下,可能会呈现个别节点并没有设置 next 值,就失败了。
  2. 但这些节点的 prev 是有值的,所以须要逆向遍历,让 prev 属性从新指向新的尾节点,直至全副自旋入队列。

4.7 acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {final Node p = node.predecessor();
            // 以后节点的前驱就是 head 节点时, 再次尝试获取锁
            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);
    }
}

当获取锁流程走到这,阐明节点曾经退出队列实现。看源码中接下来就是让该办法再次尝试获取锁,如果获取锁失败会判断是否把线程挂起。

setHead

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

在学习 CLH 偏心锁数据结构中讲到 Head 节点是一个虚节点,如果以后节点的前驱节点是 Head 节点,那么阐明此时 Node 节点排在队列最后面,能够尝试获取锁。

获取锁后设置 Head 节点,这个过程就是一个出队列过程,原来节点设置 Null 不便 GC。

shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // SIGNAL 设置了前一个节点完结唤醒,安心干别的去了,这里是睡。return true;
    if (ws > 0) {
        do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
        pred.next = node;
    } else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

你是否还 CANCELLED、SIGNAL、CONDITION、PROPAGATE,这四种状态,在这个办法中用到了两种如下:

  1. CANCELLED,勾销排队,放弃获取锁。
  2. SIGNAL,标识以后节点的下一个节点状态曾经被挂起,意思就是大家一起排队上厕所,队伍太长了,前面的谢飞机说,我去买个油条哈,一会到我了,你微信我哈。其实就是以后线程执行结束后,须要额定执行唤醒后继节点操作。

那么,以上这段代码次要的执行内容包含:

  1. 如果前一个节点状态是 SIGNAL,则返回 true。安心睡觉???? 等着被叫醒
  2. 如果前一个节点状态是 CANCELLED,就是它放弃了,则持续向前寻找其余节点。
  3. 最初如果什么都没找到,就给前一个节点设置个闹钟 SIGNAL,等着被告诉。

4.8 parkAndCheckInterrupt

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

// 线程挂起期待被唤醒    
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);
    return Thread.interrupted();}    
  • 当办法 shouldParkAfterFailedAcquire 返回 false 时,则执行 parkAndCheckInterrupt() 办法。
  • 那么,这一段代码就是对线程的挂起操作,LockSupport.park(this);
  • Thread.interrupted() 查看以后线程的中断标识。

四、总结

  • ReentrantLock 的常识比拟多,波及的代码逻辑也比较复杂,在学习的过程中须要对照源码和相干并发书籍和材料一起学习,以及最好的是本身实际。
  • AQS 的实现局部波及的内容较多,例如:state 属性应用 unsafe 提供的本地办法进行 CAS 操作,把初始值 0 改为 1,则取得了锁。addWaiter、acquireQueued、shouldParkAfterFailedAcquire、parkAndCheckInterrupt 等,能够粗疏总结。
  • 所有的 Lock 都是基于 AQS 来实现了。AQS 和 Condition 各自保护了不同的队列,在应用 Lock 和 Condition 的时候,就是两个队列的相互挪动。这句话能够细细领会。可能文中会有一些不精确或者错字,欢送留言,我会一直的更新博客。

五、系列举荐

  • volatile 怎么实现的内存可见?没有 volatile 肯定不可见吗?
  • synchronized 解毒,分析源码深度剖析!
  • ReentrantLock 之偏心锁解说和实现
  • 咋嘞?你的 IDEA 过期了吧!加个 Jar 包就破解了,为什么?
  • 大学四年到毕业工作 5 年的学习路线资源汇总
退出移动版