聊一聊JUC同步队列AQS


看了这篇文章下次有人在问你AQS你可以跟他好好扯一扯了,喜欢请点赞关注,多谢鸭

1. AQS是什么?

AbstractQueuedSynchronizer抽象同步队列简称AQS,AQS是一个FIFO的双向队列,通过同步队列来完成对同步状态的管理。它是JUC实现同步器d的重要基础抽象类。
我们先看看AQS及lock类关系图:
类关系图

2. AQS实现原理

AQS通过内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同
步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
下面看看AQS类图:
AQS
如图所示:AQS是一个抽象类,它里面有两个内部类Node和COnditionObject。还有一个子类Sync,Sync他是在ReentrantLock中的内部抽象类。
AQS的使用依靠继承来完成,子类通过继承自AQS并实
现所需的方法来管理同步状态。例如常见的ReentrantLock,CountDownLatch等AQS的两种功能从使用上来说,AQS的功能可以分为两种:独占和共享。

  • 独占锁模式下,每次只能有一个线程持有锁,比如ReentrantLock就是以独占方式实现的互斥锁
  • 共享锁模式下,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。
    很显然,独占锁是一种悲观保守的加锁策略,它限制了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它
    放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源

2.1 Node节点

2.1.1 Node是AQS中的静态内部类。

主要属性如下:

static final class Node { 
	//标记线程是获取共享资源时被阻塞挂起后放入AQS队列的
	static final Node SHARED = new Node();
	//标记线程是获取独占资源时被挂起后放入AQS队列的
	static final Node EXCLUSIVE = null;
	//
	static final int CANCELLED =  1;
	static final int SIGNAL    = -1;
	static final int CONDITION = -2;
	static final int PROPAGATE = -3;
	/**
	* 表示节点是等待状态,包含cancelled(取消),SIGNAL(线程需要被唤醒)
	* CONDITION(线程在等待condition 也就是在condition队列中)			
	* 
	*/
	int waitStatus; 
	Node prev; //前继节点 
	Node next; //后继节点 
	Node nextWaiter; //存储在condition队列中的后继节点 
	Thread thread; //当前线程 
}

AQS类底层的数据结构是使用双向链表,是队列的一种实现。包括一个head节点和一个tail节点,分别表示头结点和尾节点,其中头结点不存储Thread,仅保存next结点的引用。

2.1.2 入队操作

我们看看如何将一个线程包装成一个节点放入队列中。

    private Node enq(final Node node) {
    	//无意义的死循环,操作成功则返回
        for (;;) {
        	//(1)
            Node t = tail;
            if (t == null) { // Must initialize
            	//(2)
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//(3)
                node.prev = t;
                if (compareAndSetTail(t, node)) {	//(4)
                    t.next = node;	(5)
                    return t;
                }
            }
        }
    }
  • (1)第一次进来,t==null
  • (2)通过unsafe的CAS设置哨兵节点为头节点,如果设置成功,将为尾节点也指向头节点。
  • (3)设置前驱节点为尾部节点
  • (4)然后将当前node设置到尾部
  • (5)然后将尾部节点的后驱节点设为node,这样新添加的node节点就是尾部节点了。

2.1.3 CAS操作

在上面提到了调用unsafe类执行CAS操作
unsafe类中有很多compareAndSetXXX方法。这个是一个native方法,比如上面调用了unsafe#compareAndSwapObject方法

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  • 第一个参数为需要改变的对象;
  • 第二个为偏移量(即相对于需要改变的对象的首地址的偏移值,
  • 第三个参数为期待的值,
  • 第四个为更新后的值
    整个方法的作用是如果当前时刻的值等于预期值var4相等,则更新为新的期望值 var5,如果更新成功,则返回true,否则返回false。

AQS中,除了本身的链表结构以外,还有一个很关键的功能,就是CAS,这个是保证在多线程并发的情况下保证线程安全的前提下去把线程加入到AQS中的方法,可以简单理解为乐观锁。首先,用到了unsafe类,(Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被
广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等;Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等)。

说到这里就知道了unsafe类是有多叼了吧。
如果理解了上面,我们在看看刚刚AQS中设置头节点,请保持耐心继续往下看,小伙伴们,要深入啊…

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
//内部类Node    
static final class Node {
	//(1)头节点
	private transient volatile Node head;
	private transient volatile Node tail;
	private volatile int state;
	//其他省略
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long headOffset;
	//注意看这里!!!!!,
    static {
        try {
        	//(2)获取属性相对于类对象首地址的偏移值
        	stateOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//设置头节点
	private final boolean compareAndSetHead(Node update) {
		//(2)
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
}    

讲解:

  • (1)在内部类Node定义了属性head,
  • (2)获取属性head相对于类对象首地址的偏移值headOffset
  • (3)设置头节点调用compareAndSwapObject方法,传入headOffset参数
  • 最后通过CAS设置node到head处。
    深入之后大家感觉怎么样,明白了吧!嘿嘿
    关于unsafe类下次可以拓展写一篇文章。

2.2 AQS同步队列

下面以独占方式讲解下锁的获取与释放,默认就是独占锁。

2.2.1 尝试获取锁

#acquire尝试获取锁,方法很短,但调用了很多其他方法,你细品吧

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//(1)
            selfInterrupt();
    }
    //根据公平/非公平实现有差别,子类实现
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    private Node addWaiter(Node mode) {
     	//将当前线程创建一个独占的Node节点,mode为独占模式
        Node node = new Node(Thread.currentThread(), mode);
		//tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //防止有其他线程修改tail,使用CAS进行修改,如果失败则降级至full enq
            if (compareAndSetTail(pred, node)) {
            	//// 如果成功之后旧的tail的next指针再指向新的tail,成为双向链表
                pred.next = node;
                return node;
            }
        }
        //文章前面已讲解
        enq(node);
        return node;
    }

#acquire方法作用

  • tryAcquire 首先通过cas去修改state的状态,如果修改成功表示竞争锁成功,如果失败的,tryAcquire会返回false,
  • Node.EXCLUSIVE 表示node节点为独占的,addWaiter方法把当前线程封装成Node,并添加到队列的尾部

aqs

addWaiter返回了插入的节点,作为acquireQueued方法的入参,这个方法主要用于争抢锁

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	// 获取prev节点,若为null即刻抛出 NullPointException
                final Node p = node.predecessor();
                //如果前驱为head才有资格进行锁的抢夺,所以判断头部节点
                if (p == head && tryAcquire(arg)) {
                // 获取锁成功后就不需要再进行同步操作了,获取锁成功的线程作为新的 head节点
                    setHead(node);
                    //head节点,head.thread与head.prev为null, 但是head.next不为null
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())// 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志	
                    interrupted = true;
            }
        } finally {
        // 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
            if (failed)
                cancelAcquire(node);
        }
    }
    // predecessor 获取前驱节点
	final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
   }

获取锁成功时,头节点出队列,如下图:
获取锁成功

2.2.2 锁的释放

这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加1(当
然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;// 这里是将锁的数量减1
            if (Thread.currentThread() != getExclusiveOwnerThread())// 如果释放的线程和获取锁的线程 不是同一个,抛出非法监视器状态异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {// 直到最后一次释放锁时,才会把当前线程释放
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

2.3 ConditionObject

AQS另一个内部类是ConditionObject,用来结合锁实现线程同步。它可以访问AQS对象内部的变量,它是一个条件变量,可以让线程达到某个条件时才能被唤醒。每个条件变量对应一个条件队列。条件队列是一个单链表的队列,用来存放线程调用await方法后被阻塞的线程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章