从ReentrantLock来剖析AQS源码


一、AQS是什么?

AQS的全称是AbstractQueuedSynchronizer,翻译过来就是抽象的队列同步器
AQS是基于双向队列和一个int类型的共享变量volatile int state,来管理和控制多线程情况下操作数据,来达到数据安全性的目的。 而AQS还是java.util.concurrent (简称JUC )框架中实现的基石。JUC中不少用来解决线程安全问题的对象,这些对象的实现都需要一套共同的管理线程并发操作数据的方法实现,所以抽出来作为父类继承,也就是所谓的AbstractQueuedSynchronizer,例如:

ReentrantLock------可重入锁
ReentrantReadWriteLock------读写锁
CountDownLatch------控制线程等待
Semaphore-----信号量

在这里插入图片描述
示例:
当多个线程来访问共享变量时,线程之间会通过CAS(比较即交换)的方式来竞争state资源,AQS会把竞争到state资源的线程设置为有效线程,同时对state+1操作,当别的线程发现state=1时,就知道此时资源已经有别的线程在使用了,AQS就会把未抢到资源的线程放到队列中进行阻塞等待,直到state资源被释放,重复上述操作。

我们知道AQS中提供公平锁非公平锁俩种实现

    /**
     * ReentrantLock默认创建的是非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 传参false则创建公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们今天这里就拿ReentrantLock中实现的非公平锁在剖析AQS的源码。

二、ReentrantLock中的非公平锁实现源码

测试代码:

public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程A");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "A-thread").start();


        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程B");

            } finally {
                lock.unlock();
            }
        }, "B-thread").start();
    }

我们这里模拟俩个线程-----线程A和线程B,使用ReentrantLock 进行加锁和释放锁的操作。
让我们跟着lock.lock()方法进去来解开AQS以及ReentrantLock 的庐山真面目。
 
 
从lock.lock()进去找到ReentrantLock 对AQS的实现方法NonfairSync:

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * 尝试加锁 加锁失败则执行进去队列操作
         */
        final void lock() {
        	//compareAndSetState(0, 1) 利用CAS来抢占 AQS中的state变量
        	//原来的state的值为0,想要更改成1,返回true,说明抢占成功,state的值变为1
            if (compareAndSetState(0, 1))
            	//同时将当前线程设置为有效线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

 
可以看到NonfairSync方法中核心的就是compareAndSetState(0, 1)方法返回的结果,线程进来时第一步就是去尝试抢占state资源,如果抢占成功则返回true并且将当前线程设置为有效线程,同时state值设置为1。
如果compareAndSetState(0, 1)返回结果为false,说明竞争失败,则进入acquire(1)方法。
 
 
接下来我们看一下线程抢占不成功的情况下,acquire(1)方法做了些什么事情。

//此处的int arg承接上面 传入的值为1。
public final void acquire(int arg) {
		//我们先来解决if条件中的方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我们发现acquire(int arg)方法中,主要依靠

  1. tryAcquire(arg)
  2. addWaiter(Node.EXCLUSIVE), arg)
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

几个方法的执行结果。我们挨个来看。
 
 
tryAcquire(arg)

进入tryAcquire(arg)我们可以发现,这个方法是AQS提供的实现方法。
在这里插入图片描述
在这里插入图片描述
找到ReentrantLock对于此方法的非公平实现

        /**
         * 获取当前state变量的值再次尝试抢占
         */
        final boolean nonfairTryAcquire(int acquires) {
        	//拿到当前线程
            final Thread current = Thread.currentThread();
            //获取state变量的值
            int c = getState();
            //如果state的值为0,说明此时没有线程占用,可以抢占
            if (c == 0) {
            	//compareAndSetState(0, acquires)和上一步一样 尝试加锁,加锁成功的情况下,设置为有效线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果有效线程为当前线程,则说明重入了,则将state的值再加1操作。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //state+1后进行更新旧值
                setState(nextc);
                return true;
            }
            //如果上面抢占不成功或者不是重入则返回false
            return false;
        }

tryAcquire(arg)返回false时,则需要继续往下一个方法进行。
 
 
addWaiter(Node.EXCLUSIVE), arg)

这里要插入一个知识点,就是在AQS中,是以Node作为节点来形成双向队列的,而Node节点中有如下几个参数

    static final class Node {
        /**
         * 节点状态 默认为0
         */
        volatile int waitStatus;

        /**
         * 前置节点的指向
         */
        volatile Node prev;

        /**
         * 后置节点的指向
         */
        volatile Node next;

        /**
         * 当前线程
         */
        volatile Thread thread;
        }

 
讲解玩AQS中的Node节点属性后,我们再来看addWaiter(Node mode)方法

    private Node addWaiter(Node mode) {
    	//将当前线程封装成Node节点
        Node node = new Node(Thread.currentThread(), mode);
        //获取队列尾部节点
        Node pred = tail;
        //如果队列尾部节点不等于null的情况下
        if (pred != null) {
        	//将node的前置节点指向上一个尾节点pred 
            node.prev = pred;
            //compareAndSetTail(pred, node)设置成功后
            if (compareAndSetTail(pred, node)) {
            	//将上一个尾节点的next指向当前新加进来的node节点
                pred.next = node;
                return node;
            }
        }
        //如果尾部节点为null则进入enq(node)方法
        enq(node);
        return node;
    }

 
enq(node)
再进去enq(node)方法查看

    private Node enq(final Node node) {
    	//自旋操作
        for (;;) {
            Node t = tail;
            //初始化Node节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                	//将初始化的Node节点作为队列的头节点
                    tail = head;
            } else {
            	//自旋下一次进来 将当前node节点的前置节点指向上一次初始化的队列头节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                	//将上一次初始化的队列头结点next节点指向当前node节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

讲过分析我们发现enq(node)方法主要是来设置队列的头节点的,当我们第一次添加node节点进队列中,AQS会初始化一个Node节点作为头部节点,头节点是个虚节点,将当前节点往后放置。
 
 
当我们设置完队列中头部节点和当前node节点中的前置节点指向和后置节点指向时,将当前node节点返回至acquireQueued(final Node node, int arg)方法中,我们再来看在acquireQueued()方法中又做了什么。
 

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋操作
            for (;;) {
            	//获取当前node节点的前置节点指向
                final Node p = node.predecessor();
                //如果当前节点的前置指向节点是队列头部节点 并且tryAcquire(arg)抢占state成功
                if (p == head && tryAcquire(arg)) {
                	//则将当前节点设置为头部节点
                    setHead(node);
                    //设置为null 由GC进行回收
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //shouldParkAfterFailedAcquire(p, node)通过自旋操作来修改node节点的前置指向节点中的状态值为-1
                if (shouldParkAfterFailedAcquire(p, node) &&
                	//LockSupport.park(this);将当前线程阻塞 至此此没抢到state的线程才放入进队列中
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
            	//finally取消此node节点争抢
                cancelAcquire(node);
        }
    }

 
来看一下上述代码中的shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt()) 方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	//自旋第一次进来时 也就是node节点的前置节点状态值为0 则走下面方法将状态值设置为-1 下次自旋进来时直接返回true
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
private final boolean parkAndCheckInterrupt() {
		//将此线程阻塞 至此此线程封装成的node 才真正进入队列 等待唤醒
        LockSupport.park(this);
        return Thread.interrupted();
    }

到此acquire(int arg)方法分析完毕,我们发现在acquire(int arg)方法中,AQS做的事就是将未抢到state资源的线程放入队列中,期间node节点会不断的尝试争抢state资源,直到调用LockSupport.park(this)对其阻塞,才放弃争抢资源。同时在acquire(int arg)中AQS还做了初始化队列的头部Node,并且拼接双向队列,而放入进队列中阻塞的线程又是如何被唤醒的,我们接着往下看:

三、线程的唤醒

我们在使用Lock锁的时候,必须要在finally块中调用unlock()方法进行手动释放锁,不然会造成死锁的情况,我们就从unlock()方法中来看一下,队列中等待的线程是如何被唤醒的。

	//我们发现unlock()中调用的是AQS中release(int arg)方法进行释放锁
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //队列中头部节点不等于null和节点状态不等于0时 执行unparkSuccessor(h)方法。
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

我们来具体看下unparkSuccessor(Node node)方法

    private void unparkSuccessor(Node node) {
        /*
         * 忽略掉  直接看最下面
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 获取节点的next指向节点也就是队列中下一个将要唤醒的node节点
         */
        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;
        }
        //如果next节点不为null 则调用LockSupport.unpark(s.thread);进行线程唤醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }

到此我们就从ReentrantLock中的加锁和释放锁来把AQS的原理和ReentrantLock的原理分析走了一遍。

要是文章中有什么理解错误的地方欢迎指出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值