面试官:不会AQS?那你简历上写什么并发编程

1 篇文章 0 订阅
1 篇文章 0 订阅

关于AQS(AbstractQueuedSynchronizer),其实平时我们使用的最多的就是ReentrantLock,我们先看看ReentrantLock到底是个什么东东

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

原来ReentrantLock 是Lock的实现类之一,而且它还有一个非常重要的属性Sync,这个类可是关键哦。当我们调用ReentrantLock的Lock()方法加锁的时候发生什么?进入源码一看,原来它调用了sync的lock()方法

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

那这个sync又是什么东东呢?我们再点进源码一看,原来这个就是我们大名鼎鼎的AQS类的子类之一啊!!里面也有lock()方法

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

好啦,这里我们就看到了我们经常说的ReentrantLock原来是借助AQS实现的锁啊,那么接下来我们就要进入AQS的源码去看看啦,冲冲冲
在这里插入图片描述
这里的lock是一个抽象方法,它有不同的实现类

abstract void lock();

盲猜,也是就我们所说的公平锁和非公平锁了,毕竟面试官问你它的优势,你肯定会说它既能实现公平锁又能实现非公平锁
在这里插入图片描述
果不其然
在这里插入图片描述
我们先来看看公平锁的实现原理,公平锁的实现里面调用acquire()方法,并且传入了参数1

final void lock() {
            acquire(1);
        }

继续刨根,这里的acquire()的大概逻辑就出来了。我们先说一下大概的逻辑,再往里面深扒。首先这里tryAcquire方法是尝试获取锁,如果失败了addWaiter方法是进入一个队列,这个队列是双向链表维护的;进入队列之后,这个线程还是不得死心,它还要再挣扎一下,也就是我们通常所说的自旋(2次),于是它继续调用acquireQueued方法,最后实在拿不到锁了,它就睡眠自己了。

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

这就是AQS去加锁的一个大致流程。看到这里你可能还不觉得Doug Lea有多牛逼,但是等会你就叹服
在这里插入图片描述

我们先去看一下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;
        }

下面我们就一起畅游吧,首先拿到当前运行的线程current ,再通过getState方法获取锁的状态,这里的state属性表示锁的状态,默认为0表示无锁。同时为了保证线程之间的可见性,使用volatile关键字修饰private volatile int state;假如此时状态为0,进入第一个if判断,这里通过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());
    }

这个方法里面有两个Node节点,分别是头尾节点,初始值都为null,此时我们是第一个线程进来拿锁,所以队列并没有被初始化,因此h != tfalse,后面的&&就自然短路了。整个方法返回false
好,那就继续回到咱们上面的尝试获取锁的第一个if代码块

if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }

由于hasQueuedPredecessors方法返回false,前面加了,所以整个为true。继续判断&&后面的代码,这里就有了CAS原子操作了。compareAndSetState(0, acquires)是以CAS的方式将锁的状态从0修改为1。假如线程拿到了锁,返回true,进入下面的代码块setExclusiveOwnerThread(current),这个方法就是为了把当前线程设置为拿到锁的线程,为啥呢?后面在判断可重入锁的时候用。最后整个tryAcquire方法返回了true
该线程拿到了锁,进入了我们的业务逻辑代码开始执行…
这里只是模拟第一个线程来拿锁的情况,怎么样,挺得住吗
在这里插入图片描述
接下来咱们开始看看面试官都喜欢问的可重入锁,其实很简单,也在咱们的tryAcquire方法中。假如拿到锁的线程在业务逻辑代码中需要再次拿到锁,依旧会走上面的流程来到咱们的tryAcquire中,只不过此时的state已经被它自己修改为了1,所以进入这个代码块

  else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

首先判断当前线程是不是持有锁的线程啊?毋庸置疑,肯定是的,那就把锁的状态+1就完了,然后返回true,业务逻辑代码正常执行。怎么样?是不是很简单。原来可重入的意思就是线程可以再次拿到锁,只不过把锁的状态值增加1而已。
上面的过程你是不是感觉很简单嘛?确实,假如面试管问你AQS,你首先告诉他,多线程在交替执行的时候,AQS的队列并没有初始化,也没什么卵用。 他可能会觉得,这小子可能真看过源码
在这里插入图片描述
接下来咱们讲点有趣的。多线程不再交替执行,也就是说存在竞争了,那咋办?Doug Lea都给你安排的明明白白的,不慌。
第一个线程在执行,第二个线程来了,它也会进入tryAcquire方法,然后它发现锁的状态不为0,同时自己也不是持有锁的线程,那它只能可怜的拿到最后一行代码,返回false。接下来主场戏就要开始了,真的精彩,有尿你都憋住!!!
还记得acquire方法吗

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

此时tryAcquire返回false,前面加了,所以为true,于是代码继续往后判断,成功进入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;
    }

首先把当前线程封装为一个Node节点,然后判断tail节点是否为null,这里首尾节点都没有初始化,肯定为null啊,所以直接走enq(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;
                }
            }
        }
    }

这里面是一个自旋的方法,第一次自旋的时候,先通过CAS的方式创建一个空节点,并让head指向空节点,之所以叫空节点,是因为里面的threadnull。然后尾节点也指向这个空节点。然后再自旋一次,通过CAS的方式让当先线程所在的节点挂到空节点后面。
到此为止,第二个线程做了什么事情呢?首先去拿锁,拿不到;然后入队列,发现队列未初始化,自己去做了初始化的工作,并且把自己挂到了虚拟头节点的后面。接下来他该干嘛了呢?他要准备睡眠自己了,不过,他可不得这么轻易睡觉,你上床睡觉不得在被窝里挣扎一下哇???
在这里插入图片描述
话不多说,来人,上源码
进入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);
        }
    }

第一感觉就是又在自旋对不对,是的AQS就是自旋+CAS+双向队列+park。第一次自旋,先拿到当前线程所在Node节点的前一个节点final Node p = node.predecessor(),然后判断前一个节点是不是虚拟头节点,如果是的话,他就去尝试获取锁(第一次挣扎),这里我们假设他获取锁失败。于是进入下面的方法shouldParkAfterFailedAcquire(p, node),这个方法是在干嘛呢?大胆点进去

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

不要慌,代码很长,逻辑很短,全是注释,咱们一步一步来分析
首先,会拿到前一个节点的waitStatus,咱们简称ws。然后就是判断ws的值,ws默认为0,所以第一次自旋进来,当前线程会把他前面的节点的ws状态使用CAS的方式修改为-1,就是这句代码compareAndSetWaitStatus(pred, ws, Node.SIGNAL),然后返回一个大大的false。由于返回false,所以

 if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())

不会继续往下执行;代码进入第二次自旋,第二次自旋我们又假设没有获取到锁,(不是线程二不给力,只是后面再专门讲在竞争情况下拿到了锁该怎么维护这个双向队列,忍耐一下)所以再次进入shouldParkAfterFailedAcquire方法,由于第一次自旋的时候ws已经被修改为-1了,所以这次直接返回true。然后就执行后面的if语句后面的代码块了

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

也就是说park当前线程,是线程睡眠。
到这里我们发现,第二个线程在睡眠之前通过两次自旋又做了两件事情,第一件:把自己前一个节点ws修改为-1,第二件就是自己睡眠。
其实为啥也这样设计呢? 我自己感觉哈,给head虚拟节点两次挣扎的机会,就是为了进来不让线程park,这会设计用户态和内核态的切换,十分消耗性能。我们一直抱有侥幸心里,总觉得可能我刚入队,前面一个线程就释放锁了呢。那我是不是可以不用睡眠,直接执行呢?这种情况确实可能存在。但是我们是公平锁,只有head节点的后面第一个节点可以去尝试获取锁,其它后面进来的节点你就早点洗洗睡吧,你前面的都还是排队呢,你猴急啥。选择两次自旋也是出于对性能的考虑,如果自旋太多,会十分影响CPU的性能。这个设计真的非常巧妙。

后面再来第三个、第四个线程等等都是这样自旋两次,唯一不一样的就是在acquireQueued方法中,因为自己的上一个节点不是head虚拟节点,所以不会执行tryAcquire方法,但是依旧会修改前面节点的ws,然后睡眠。这里面我们模拟了线程交替执行获取锁,模拟了第一个线程占用锁,后面所有的线程拿不到锁入队列的过程,就问你Doug Lea强不强???
在这里插入图片描述
听到这里,可能有小伙伴要为线程二正名了,为什么他就那么苦逼,每次拿不到锁。好的,那这次咱们就让他硬气一回,让他拿到锁,当队列中的Node拿到锁了又该如何去维护这个链表呢?
愣着干嘛??上菜了
假如线程二在自旋的过程中拿到锁了。开心,终于轮到我执行了

  if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }

进入到if代码块中,首先是setHead方法,咱们去看看他干了什么事情

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

这个方法就是把head节点指向获取到锁的节点,然后把节点中的thread置为null,然后把他的前面阶段断开(这个以前的head虚拟节点就没有了引用,就会被GC回收了)。
看懂了吗?巧妙吗?折服了吗?
拿到锁的线程去执行了,然后他所处的Node变为了head虚拟节点,简直太优秀了!!!

到此,咱们公平锁的基本上就讲完了,这才讲了一半,哈哈哈,不过相信你已经可以自己去分析非公平锁的源码了,一步一步进去看,肯定可以看明白的

其实非公平锁也不难,我们大概看一下

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

这是他的lock方法,每个线程一上来,不管队列中有没有人派对,自己先去抢占锁,抢不到再说
在这里插入图片描述
后面有时间再阅读解锁和唤醒的源码。这是自己看了源码的一些心得体会,自己记录看一下,同时也希望帮助到正在啃源码的你。有错的地方可以在评论区留言交流
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值