Java 并发编程AQS–源码解读

导读:本篇文章讲解 Java 并发编程AQS–源码解读,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

AQS主要通过独占式、共享式同步状态的获取和释放,接下来我们来看下AQS代码是如何实现的

公平锁与非公平锁

  • 在公平锁的情况下,会先去判断线程是否需要排队,如果是的话会进行排队,不会进行抢锁操作,乖乖的进入队列进行排队,否则会进行抢锁操作
  • 在非公平锁的情况下,无论队列是否有线程在排队线程都会先去尝试获取同步状态,直接进行抢锁操作,成功则直接运行,否则进入到同步队列,后续的流程同公平锁一致

Node节点

在AQS中,是使用队列的方式来实现同步管理的,我们先来认识下队列中的Node节点数据结构
在这里插入图片描述

static final class Node {
    /**
     * 共享模式节点
     */
    static final Node SHARED = new Node();

    /**
     * 独占模式节点
     */
    static final Node EXCLUSIVE = null;

    /**
     *  CANCELLED值,表明线程已取消
     */
    static final int CANCELLED = 1;

    /**
     * SIGNAL值,表明后续线程需要释放
     */
    static final int SIGNAL = -1;

    /**
     * CONDITION值,指示线程正在等待条件
     */
    static final int CONDITION = -2;

    /**
     * PROPAGATE值,指示下一个acquireShared应该无条件传播
     */
    static final int PROPAGATE = -3;

    /**
     * 等待状态
     */
    volatile int waitStatus;

    /**
     * 上一个节点
     */
    volatile Node prev;

    /**
     * 下一个节点
     */
    volatile Node next;

    /**
     * 进入该节点队列的线程。构造时初始化,使用后为空。
     */
    volatile Thread thread;

    /**
     * 条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量,
     * 也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段
     */
    Node nextWaiter;

    /**
     * 是否为共享模式
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    // 返回当前节点的prev node
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null) {
            throw new NullPointerException();
        } else {
            return p;
        }
    }

    Node() {    // 用于建立初始头或共享标记
    }
    
    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;
    }
}

在Node节点中包含:

  • thread:一个等待获取同步状态的线程
  • prev:指向上一个节点的引用
  • next:指向下一个节点的引用
  • waitStatus:等待的状态
    • CANCELLED:值尾1,表示在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,接入CANCELLED状态后将不会再变化
    • SIGNAL:值尾-1,表示后续节点的线程处于等待状态,如果当前节点的线程释放了同步状态或者被取消便会通知后续节点,使后续节点的线程得以运行
    • CONDITION,值为-2 表示节点再条件队列中,节点等待线程再Condition上,当其它线程队Condition调用了signal()后,该节点将会从条件队列中转移到同步队列中
    • PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件传播下去
  • nextWaiter:表示条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量static final Node SHARED = new Node();,也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段
  • EXCLUSIVE:独占模式节点

同步队列结构

在这里插入图片描述
关键点:

  1. FIFO队列,先入先出
  2. 所有节点通过prev、next引用指向下一个节点
  3. 等待的节点通过LockSupport.park(this)来阻塞线程,让线程等待
  4. 当线程1执行完成后会唤醒后续等待的线程(状态需为SIGNAL,否则为CANCENEL则跳过)
    所有线程要获取锁都是通过state来进行判断:
    • 如果是等于0说明没有其它线程获取锁,可以被获取,获取后会设为1并将exclusiveOwnerThread设置为获取到锁的线程
    • 如果不是等于0则判断是否是exclusiveOwnerThread的线程,是的话则获取锁,state+1,否则进入等待队列进行排队

独占式同步状态的获取和释放

获取同步状态(acquire)

acquire 获取同步状态方法入口

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            // 设置当前线程的中断标识
            selfInterrupt();
        }
    }

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列自旋等待的相关工作,主要流程如下:

  1. 首先调用子类的tryAcquire方法,该方法是线程安全的获取同步状态,子类实现

  2. 如果获取失败,通过addWaiter方法构造独占式同步节点并将该节点加入到同步队列的尾部

     /**
     * 为代表当前线程并指定模式为mode的节点创建/进入队列。
     */
    private Node addWaiter(Node mode) {
        // 将当前线程构造成Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试快速在尾节点后新增节点 提升算法效率 先将尾节点指向pred
        Node pred = tail;
        if (pred != null) {
        	// 尾节点不为空 当前线程节点的前驱节点指向尾节点
            node.prev = pred; 
            // 并发处理 尾节点有可能已经不是之前的节点 所以需要CAS更新
            if (compareAndSetTail(pred, node)) {
            	// CAS更新成功 当前线程为尾节点 原先尾节点的后续节点就是当前节点
                pred.next = node; 
                return node;
            }
        }
        //第一个入队的节点或者是尾节点后续节点新增失败时进入enq
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
    	for (;;) {
    		Node t = tail;
    		if (t == null) {
    			//尾节点为空 第一次入队 设置头尾节点一致 同步队列的初始化
    			if (compareAndSetHead(new Node()))
    				tail = head;
    			} else {
    				//所有的线程节点在构造完成第一个节点后 依次加入到同步队列中
    				node.prev = t;
    				if (compareAndSetTail(t, node)) {
    					t.next = node;
    				return t;
    			}
    		}
    	}
    }
    
  3. 然后调用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);
            }
        }
    

    再来看看shouldParkAfterFailedAcquireparkAndCheckInterrupt方法是如何阻塞当前线程的

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驱节点的状态决定后续节点的行为
       int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点为-1 后续节点可以被阻塞
             */
            return true;
        if (ws > 0) {
            /*
             * 前置节点被取消了,跳过前置节点并继续重试,直到前置节点waitStatus<0,也就是不是取消状态的。
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 前驱节点是初始或者共享状态就设置为-1 使后续节点阻塞
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    private final boolean parkAndCheckInterrupt() {
        //阻塞线程
        LockSupport.park(this);
        return Thread.interrupted();
    }
    

    节点自旋的过程大致示意图如下:
    在这里插入图片描述
    在获取同步状态成功后,当前线程就会从acquire方法返回

释放锁(release)

public final boolean release(int arg) {
    if (tryRelease(arg)) {//同步状态释放成功
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //直接释放头节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

	// 没有后续节点或后续节点为CANCELLED
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾节点开始向头节点遍历,遍历到整个队列最前排的waitStatus<=0的节点,赋值给s,用于后续的unpark操作。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒后续节点执行自旋操作
        LockSupport.unpark(s.thread);
}

释放过程简单明了

独占式同步状态的获取和释放总结:

  1. 获取同步状态的时候,同步器维护一个同步队列
  2. 获取失败的线程会被加入队列中并进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态
  3. 在释放同步状态时,同步器调用tryRelease方法释放同步状态,然后唤醒头节点的后置节点

共享式同步状态的获取和释放

共享式的主要特点在于state的值,在初始化Sync的时候,会去设置AQS中的state值,state是多少就代表可以同时多少个线程获取该锁,比如实例化Semaphore的时候

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    
    static final class NonfairSync extends Sync {
        NonfairSync(int permits) {
            super(permits);
        }
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) {
        	// 设置state的初始值
            setState(permits);
        }
    }

获取同步状态(acquire)

public final void acquireShared(int arg) {
    //获取同步状态的返回值大于等于0时表示可以获取同步状态
    //小于0时表示可以获取不到同步状态  需要进入队列等待
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
 	// 和独占式一样的入队操作
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 前驱结点为头节点且成功获取同步状态 可退出自旋
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    // 退出自旋的节点变成首节点
    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
        	// 唤醒后续等待的节点
            doReleaseShared();
    }
}

释放同步状态(release)

与独占式一样,共享式获取也是需要释放同步状态,通过调用releaseShared方法可以释放同步状态,代码如下:

public final boolean releaseShared(int arg) {
    // 释放同步状态
    if (tryReleaseShared(arg)) {
        // 唤醒后续等待的节点
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
     // 自旋
   for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    continue;
                }
               	// 唤醒后续节点
          unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}

unparkSuccessor方法和独占式是一样的。

共享锁和非共享锁的区别?

共享模式比独占模式多做了一步操作,就是调用了doReleaseShared方法,去唤醒队列中所有共享模式的节点,让这些线程在去争夺共享资源,而独占式是没有这个操作的。

后续

更多的AQS可以参考下一篇文章:ReentrantLock源码解析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17839.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!