Java中J.U.C包下锁的基础-AQS分析

1 篇文章 0 订阅

目录

序言:

1:什么是AQS

2:ReentrantLock(独享式)

3:Semaphore(共享式)


序言:

对于锁的作用,简单保证临界区(多个线程,进程同时访问的区域,最终我们希望只有一个线程进去执行操作的区域)中数据的一致性,不会因为在并发的时候出现脏数据,错乱数据。

对于java中存在的锁有两种,第一个以Synchronized关键字,第二也是今天重要说明的以AQS(AbstractQueuedSynchronizer)为核心的基于此框架衍生的各种锁。

在java中我们可以从不同的纬度来看待锁,如下脑图所示:

我们可以看到除了Automi各实现,以及Synchronized关键字,基本上java中的锁都与AQS有关。

1:什么是AQS

它全名为AbstractQueuedSynchronizer,提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架(来源作者注释),上图所示的ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch等并发类均是基于AQS来实现的,它们通过AQS实现其模板方法(这里典型的设计模式中的-模版方法模式,说明主要的核心还是AQS来进行实现),来实现不同的场景中的应用。

AQS中通过被volatile修饰的state的大小来代表锁是否被占有(一般0代表为被占有,1占有。作为共享锁可以记录共享线程的数量,作为可重入锁也可以被重入的次数),通过FIFO双端队列来存储等待线程(被阻塞的线程在该队列中等待去获取锁,在这其中即使线程被Interrupt中断也会会阻塞知道线程获取锁后才继续中断)

这里我们通过ReentrantLock(独享式), Semaphore(共享式)来介绍这AQS在其中起到的作用

2:ReentrantLock(独享式)

ReentrantLock源码,当我们创建一个ReentrantLock对象时我们有两种选择,第一创建一个公平锁以及创建一个非公平锁(默认是非公平一个锁)

ReentrantLock reentrantLockNoFair = new ReentrantLock();

ReentrantLock reentrantLockFair = new ReentrantLock(true);
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

而FairSync与NonfairSync为ReentrantLock两种实现,都继承了Sync最终继承AbstractQueuedSynchronizer(AQS)

当我们调用lock()上锁时

/**
     * 非公平锁
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        final void lock() {
            //非公平锁 在获取锁时直接插队 尝试将state变更为
            //compareAndSetState AQS第一个方法 该方法实际调用
            if (compareAndSetState(0, 1))
                //将当前线程置为独享锁拥有对象
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //若果抢占失败 其实和公平锁的做法一样了 调用的是AQS中函数
                acquire(1);
        }

    }

    /**
     * 公平锁
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            //直接调用AQS中函数
            acquire(1);
        }

    }

我们可以看到使用AQS中2个方法:

compareAndSetState():本质是调用unsfae类中的CAS方法,通过CAS原子性操作将数据写入
acquire():以独占模式获取锁,忽略线程中断

查看acquire()源码如下所示:

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

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

发现acquire为final修饰使得该方法无法被重写,而tryAcquire()函数直接抛出异常,实际上这里使用了模版方法模式,acquire定义为 需要执行的模版,上文中也存在4个AQS中的函数:

   tryAcquire():尝试获取独享锁,是需要子类进行重写的,可以根据该函数不同的实现来定义锁的类型,公平锁或非公平锁

  addWaiter():  因为获取锁失败所以将当前线程以node节点的方式,追加到双端列表的尾部

  acquireQueued():线程在队列中不间断(死循环)的方式等待获取锁,如果该线程被外部中断也要等待获取锁之后通过selfInterrupt()执行中断操作

在上文的FairSync与NonfairSync中对tryAcquire进行了重写如下所示:

    static final class NonfairSync extends Sync {
       
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    //非公平锁获取独享锁
    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;
        }

    //公平锁尝试获取独享锁
    protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取锁标示符状态
            int c = getState();
            if (c == 0) {
                //未有线程获取到锁
                // hasQueuedPredecessors()AQS中函数-判断当前如果当前队列为空 
                // 如果为空 那么此时可以尝试直接获取锁
                if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                    //获取到锁了 当前线程写入锁中
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //已经存在锁了
            else if (current == getExclusiveOwnerThread()) {
                //判断是否是当前线程所有 可重入锁的实现 没获取该锁一次 state+1 释放-1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

addWaiter()源码分析:

    /**
     * 给定模式(独享或共享模式)下为当前线程在队列尾部创建新的节点 线程排队
     *
     */
    private Node addWaiter(Node mode) {
        //创建节点
        Node node = new Node(Thread.currentThread(), mode);
        //尝试的节点快速插入等待队列队尾,若失败则执行常规插入(enq方法)
        //尾node
        Node pred = tail;
        if (pred != null) {
            //尝试将当前节点与tail节点相连 若成功直接返回
            node.prev = pred;
            //尾部替换
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //出现竞争cas插入失败 使用enq方法
        enq(node);
        return node;
    }

enq()源码分析:

/**
     * 将节点插入队列,进行初始化
     */
    private Node enq(final Node node) {
        //自旋将节点插入到队列中
        for (;;) {
            //获取tail
            Node t = tail;
            if (t == null) { 
                //tail为null 说明该队列不存在 初始化队列 赋值head与tail
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //不断尝试将节点插入对尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述中不论是常规插入还是快速插入本质都是通过unsafe中的cas操作来实现,该方法为native方法,由计算机CPU的cmpxchg指令来保证其原子性。

acquireQueued()源码分析:

/**
     * 线程在队列中不间断自旋(死循环)+阻塞的方式等待获取锁,如果该线程被外部中断只会记录
     * 还是要等待获取锁之后通过selfInterrupt()执行中断操作
     *
     * @param node the node
     * @param arg 锁标准位
     * @return {@code true} if interrupted while waiting
     */
    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)) {
                    //将自己置为head 严格按照FIFO原则
                    setHead(node);
                    //断开与上一节点连接 避免产生内存泄露
                    p.next = null; // help GC
                    //获取锁成功
                    failed = false;
                    //返回中断标示
                    return interrupted;
                }
                //shouldParkAfterFailedAcquire 根据上一个节点判断是否需要阻塞当前节点的线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                        //parkAndCheckInterrupt()阻塞当前线程并检查线程是否被中断过 若被中断过变更中断标示位
                        parkAndCheckInterrupt())
                    //变更中断标示状态
                    interrupted = true;
            }
        } finally {
            if (failed)
                //该线程 放弃获取锁
                cancelAcquire(node);
        }
    }

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

我们依赖于上一节点的waitStatus-当前线程等待状态,来判定当前节点是否可以竞争锁还是需要进行阻塞等待,关于此状态在Node类中进行了定义,如下所示:

	static final class Node {
        /** 表示节点正在共享模式下等待的标记 */
        static final Node SHARED = new Node();
        /** 表示节点正在独占模式下等待的标记 */
        static final Node EXCLUSIVE = null;

        /** 当前线程已取消获取state对应的waitStatus值 实际上是线程因timeout和interrupt而放弃竞争state */
        static final int CANCELLED =  1;
        /** 表示当前节点需要唤醒后续线程 代表当前节点是一个可正常执行的节点流程 当该节点释放state或者取消竞争之后,将通知后续节点可以竞争state*/
        static final int SIGNAL    = -1;
        /** 表征线程正在等待触发条件(condition)  当前节点处于条件队列中,它将不能用作同步队列节点,直到其waitStatus被重置为0 */
        static final int CONDITION = -2;
        /**
         * 下一个acquireShared应无条件传播的
         */
        static final int PROPAGATE = -3;

        /**
		 *
		 *记录当前节点状态 对应上述4种情况对应的值
		 *若都不为上述情况 则指定为0
         *
         **/		 
        volatile int waitStatus;

        //当前节点上一个节点 前继节点
        volatile Node prev;

       
	//当前节点下一个节点 后继节点 
        volatile Node next;

	//存储待竞争的线程
        volatile Thread thread;

	//链接下一个等待条件触发的节点
        Node nextWaiter;

        /**
         * 返回当前是否为共享类型
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 获取前继节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter 函数
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition 函数中
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

此时再来看待shouldParkAfterFailedAcquire函数:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前继节点的waitStatus状态
        int ws = pred.waitStatus;

        if (ws == Node.SIGNAL)
            /*
             * 如果前继节点状态为signal 说明前继节点是一个正常节点
             * 那么该节点会通知下一节点你可以获取state了 这样就不需要后续的自旋寻求前继节点状态了
             *通过返回true 来触发后续parkAndCheckInterrupt()执行从而达到线程阻塞的效果
             */
            return true;
        if (ws > 0) {

            /*
             *上文对waitStatus描述只有当CANCELLED=1(放弃的状态) 才满足ws>1情况
             *直接从该节点向前遍历 直到前置节点中的状态不为CANCELLED
             *通过该动作使得我们的节点可以向前移动
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 当前节点不为上述两个节点 通过CAS修改前继节点状态为 SIGNAL 正常状态 使得后续自旋的时候 可以通过parkAndCheckInterrupt阻塞线程
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
parkAndCheckInterrupt()函数较为简单如下所示:
private final boolean parkAndCheckInterrupt() {
        //通过LockSupport阻塞线程 查看该源码本质还是通过Unsafe类中的park来进行阻塞的
        LockSupport.park(this);
        //被前继节点唤醒时判断是否该线程被中断了
        return Thread.interrupted();
    }

上述描述完获取锁的动作,那么释放锁是如果做到的呢,它又做了其它什么事情呢,如下所示:


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

    public final boolean release(int arg) {
        //tryRelease 释放独占锁由AQS子类实现
        if (tryRelease(arg)) {
            //获取head节点 只有head才会才拥有锁
            Node h = head;
            //说明当前FIFO队列中存在等待线程       
            if (h != null && h.waitStatus != 0)
                //AQS中函数 用于唤醒该节点下一个节点中等待的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    //AQS子类中释放锁实现
    abstract static class Sync extends AbstractQueuedSynchronizer {
        
        protected final boolean tryRelease(int releases) {
            //获取最终state状态这里是为了兼容可重入锁 释放一次那么对应锁数-1
            int c = getState() - releases;
            //验证该锁是否由创建锁线程进行释放
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                 //锁释放成功 重置锁拥有线程
                free = true;
                setExclusiveOwnerThread(null);
            }
            //重写state
            setState(c);
            return free;
        }

       
    }

    private void unparkSuccessor(Node node) {
            //获取当前节点
            int ws = node.waitStatus;
            //非0的waitStatus代表该线程仍然会竞争state(除了1的CANCELLED)
            if (ws < 0)
                //通过CAS将waitStatus 初始0状态
                compareAndSetWaitStatus(node, ws, 0);

            /*
             * 需要被unpark的线程会保存在后续节点中 一般是下一个节点。
             * 如果该节点被取消或为空,则直接向后遍历,直到找到未取消(waitStatus=CANCELLED)的节点
             */
            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;
            }
            //获取该节点对应的被阻塞的线程 进行unpark唤醒 
            if (s != null)
                LockSupport.unpark(s.thread);
        }

看完上述的描述,我们将整体的加锁和解锁的动作一起来分析整体的流程,如下所示:

  1. 当一个新的线程进行lock加锁,那么此时根据自身的实现,如果为公平锁,那么严格遵守FIFO队列原则,通过CAS自旋插入到队尾,在插入队尾之后判定该节点对应的前节点是否为head,若是此时可以尝试获取下锁(若成功则说明前head已经释放过锁),否则判定前节点是否是一个正常节点(不为被取消的节点类型),若是通过LockSupport.park()进行阻塞,(此时若当前线程被interrupt中断,这里也不会进行处理,只有当该线程获取到锁之后才会进行interrupt中断,也就是说线程一旦进入锁排队中就无法被nterrupt中断),只有若不是则向上循环寻找到正常的节点。如果为非公平锁,那么该线程有两次机会和排在FIFO中head节点去抢占state,如果抢占失败那么后面的流程与上述公平锁一致了
  2. 当一个锁被线程释放了,例如node1被释放了,那么就会通过LockSupport.unpark()唤醒它下一个节点中的线程去占有锁。这里唤醒的节点需要判定它的状态是否为正常的状态,有些锁可能因为设置超时时间而导致锁被取消了,那么这种节点需要被舍弃,那么就要向下需要到第一个正常状态节点进行唤醒

3:Semaphore(共享式)

使用方式如下所示:

//定义信号量中最大共享锁数
        Semaphore semaphore = new Semaphore(10);
        try {
            //使用信号量同一时间只有指定数量的线程可以访问 可以做到线程级别的限流
            //获取共享锁
            semaphore.acquire();
            //执行的业务代码
            //释放锁
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

与ReentrantLock一样,Semaphore也有两种锁模式,公平锁与非公平锁,默认是一个非公平锁实现

    //permits 设定最大许可证数
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    //permits 设定最大许可证数 fair true公平锁 false非公平锁
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

当执行semaphore.acquire();

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

     public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //判定当前线程是否被中断 中断抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //tryAcquireShared()AQS中方法与tryAcquire()对应用于获取共享锁 由具体子类实现
        //返回值值为负值未获取到许可证
        if (tryAcquireShared(arg) < 0)
            //共享可中断模式进行阻塞
            doAcquireSharedInterruptibly(arg);
    }



    //非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }
        //调用Sync中nonfairTryAcquireShared
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }


    final int nonfairTryAcquireShared(int acquires) {
        //自旋
        for (;;) {
            //获取目前许可证数量
            int available = getState();
            //占有一份 许可证-1
            int remaining = available - acquires;
            //通过CAS自旋将剩余许可证数量写入 并返回
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
        }
    }


    //公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShar(int acquires) {
            //CAS自旋写入
            for (;;) {
                //hasQueuedPredecessors()判断是否存在FIFO队列并且head不为空 AQS中方法 因为是公平锁 所以需要判定是否已有线程等待 严格按照FIFO
                if (hasQueuedPredecessors())
                    return -1;
                //通过CAS自旋将剩余许可证数量写入 并返回 与非公平锁动作一致
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                        compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

当前线程若未获取到许可证,通过AQS中doAcquireSharedInterruptibly()以共享可中断的方式进行阻塞,源码如下所示:

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
		//创建类型为共享模式的节点插入队尾
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
		//自旋
            for (;;) {
				//获取当前节点前节点
                final Node p = node.predecessor();
                if (p == head) {
				//前节点为head时再次参数获取许可证
                    int r = tryAcquireShared(arg);
					//成功获取 变更当前节点为head 并返回
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
				//判定当前节点与之前对应前一节点必须状态为正常
                if (shouldParkAfterFailedAcquire(p, node) &&
					//阻塞当前线程 在被幻想后判断是否被中断 若中断抛出异常
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
			//说明获取许可证失败
            if (failed)
				//做些清除动作
                cancelAcquire(node);
        }
    }

 释放锁动作:

    //释放锁
    public void release() {
        sync.releaseShared(1);
    }

    //释放共享锁
	public final boolean releaseShared(int arg) {
		//释放锁 -实际上就是对许可证+1操作
        if (tryReleaseShared(arg)) {
			//唤醒FIFO下一个中等待的阻塞线程
            doReleaseShared();
            return true;
        }
        return false;
    }
	
	
	abstract static class Sync extends AbstractQueuedSynchronizer {

		protected final boolean tryReleaseShared(int releases) {
	
            for (;;) {
				//获取最新的许可证数量
                int current = getState();
				//并更后的许可证数量
                int next = current + releases;
                //校验合法性
				if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                //乐观锁 CAS自旋将最新的值写入
				if (compareAndSetState(current, next))
                    return true;
            }
        }	
	}

以上述ReentrantLock与Semaphore源码简单分析就已经可以看出AQS的作用,它通过volatile state+FIFO双端队列+CAS自旋+LockSupport线程阻塞唤醒等机制结合一起来实现sync锁的效果(功能比sync锁更加丰富),而使用者只需要去实现具体的获取锁与释放锁的逻辑即可,它隐藏了复杂的线程阻塞排队机制。

4:ReentrantLock与Synchronized性能对比

ReentrantLock对比之前描述的Synchronized的性能,ReentrantLock对于锁的获取与释放,已经在如何因对锁冲突是通过CAS自旋,也就是悲观锁的实现来处理的,而Synchronized在高并发下会升级为重量级锁通过monitorenter和monitorexit这两个字节码指令来保证临界区的安全。个人感觉在大量并发下Synchronized的性能应该会高于ReentrantLock的。为此做了一个小实验如下所示:

最终的结果:

后面的100,300代表使用线程数,前面代表执行完所有操作后对于的时间,从数据来看并发越大 synchronized要比ReentrantLock性能要高点。

以上只代表个人观点,如有错误希望大佬批评指正。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值