AQS原理分析

AQS涉及到的一些概念

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

AQS原理分析_第1张图片
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对state值的修改。

重入锁概念:是通过为每个锁关联一个请求计数器state和一个占有它的线程。当state为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将state置为1 。
如果同一个线程再次请求这个锁,state将递增;
每次占用线程退出同步块,state将递减。直到计数器为0,锁被释放。

AQS的基本数据结构—Node,即为上边CLA变体队列的节点。
Node中有几个参数:

方法属性 含义
waitStatus 当前节点在线程中的撞他
thread 该节点处线程
prev 前驱节点
predecessor 返回当前驱动点,没有抛出npe
nextWaiter 指向下一个处于CONDITION状态的节点
next 后驱节点

线程的两种模式

模式 含义
SHARED 表示线程以共享的模式等待锁
EXCLUSIVE 表示线程正在以独占的方式等待锁

waitStatus有下面几个枚举值:

枚举 含义
0 当一个Node被初始化的时候的默认值
CANCELLED 为1,表示线程获取锁的请求已经取消了
CONDITION 为-2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE 为-3,当前线程处在SHARED情况下,该字段才会使用
SIGNAL 为-1,表示线程已经准备好了,就等资源释放了

源码分析

ReentrantLock中的lock调用ReentrantLock类中的静态抽象内部类Sync,Sync继承于AbstractQueuedSynchronizer

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

AQS原理分析_第2张图片
而对Sync中lock方法的又由两个内部类FairSync公平锁类NonfairSync非公平锁类继承实现重写。
以非公平锁为例,这里主要阐述一下非公平锁与AQS之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。
AQS原理分析_第3张图片

公平锁中lock()对方法重写,直接调用acquire()方法。

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

非公平锁会先去尝试锁是否可以被占用,返回true的时候将当前线程设置为锁的占有线程。否则执行acquire()方法。

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

下面是acquire()方法的实现
其中主要是调用了tryAcquire()addWaiter()acquireQueued() 三个方法。

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

tryAcquire()方法尝试获得锁,获取成功返回true,返回false则执行addWaiter()方法添加队列

非公平锁的tryAcquire()方法

        protected final boolean tryAcquire(int acquires) {
     
            return nonfairTryAcquire(acquires);
        }
        final boolean nonfairTryAcquire(int acquires) {
     
            final Thread current = Thread.currentThread();//获取当前调用线程
            int c = getState();//获取锁状态 0为未占用,1位占用
            if (c == 0) {
     
            //如果当前调用线程可以占有锁则返回true
            //其中公平锁中在此多了!hasQueuedPredecessors()方法判断
                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;
        }

hasQueuedPredecessors()方法是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回false,说明当前线程可以争取共享资源;如果返回true,说明队列中存在有效节点,当前线程必须加入到等待队列中。

    public final boolean hasQueuedPredecessors() {
     
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

看到这里,我们理解一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?

双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当h != t时: 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了tail指向head,没有将Head指向Tail,此时队列中有元素,需要返回True(这块具体见下边代码分析)。 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。

在尝试获取锁失败后会继续执行addWaiter(Node.EXCLUSIVE)方法
实现过程如下:

  • 通过当前的线程和锁模式新建一个节点。
  • 定义pred变量指向尾节点Tail。
  • 将实例化中node的Prev指向pred
  • 通过compareAndSetTail()方法,完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。
addWaiter(Node.EXCLUSIVE)
    private Node addWaiter(Node mode) {
     
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
     
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
     
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    private final boolean compareAndSetTail(Node expect, Node update) {
     
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

如果没有被初始化,需要进行初始化一个头结点出来,便通过调用enq()方法实现。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。

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

addWaiter方法把对应的线程以Node的数据结构形式加入到双端队列里,返回的是一个包含该线程的node,然后这个node作为入参执行acquireQueued()方法。
当一个线程获取锁失败了,被放入等待队列,acquireQueued()方法会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。

	acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

    final boolean acquireQueued(final Node node, int arg) {
     
    	//标记是否拿到资源
        boolean failed = true;
        try {
     
        	//标记是否被中断过
            boolean interrupted = false;
            //开始自旋
            for (;;) {
     
            //node.predecessor()方法返回上前驱节点
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
     
                //这里判断的是上一个节点是否是头结点并且当前调用线程尝试获得锁(存在头结点是虚节点)
                //能进来表示上一个节点为头节点但锁已经被当前调用线程获取
                //把当前调用线程节点设置为头结点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //执行到这里有两种情况,一种是p不是头结点
                //另外一种是p是头结点,但是还在执行中没有释放锁(或者锁被非公平锁抢了)
                //这时候就要判断当前节点是否需要阻塞,防止无线循环浪费资源
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
     
        // 若获取失败,取消当前线程的请求
            if (failed)
                cancelAcquire(node);
        }
    }
    private void setHead(Node node) {
     
        head = node;
        node.thread = null;
        node.prev = null;
    }
    //注:setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。

前驱节点处于唤醒状态,那么当前调用线程处于阻塞。
前驱节点未阻塞,往前继续查找并清除超时、取消的节点。
如果找到有处于唤醒状态的节点,那么当前调用线程节点阻塞,否则设置当前线程为唤醒,且循环尝试获取锁。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
  	//ws == Node.SIGNAL 即ws == -1表示该线程的后续线程需要阻塞,即前一个线程节点处于占用锁状态
            return true;
        if (ws > 0) {
     
            do {
     
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //do while循环中清除了队列中前驱节点之前的取消节点
            pred.next = node;
        } else {
     
        	//设置前驱节点为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()方法主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。

private final boolean parkAndCheckInterrupt() {
     
    LockSupport.park(this);
    return Thread.interrupted();
}

上述方法的流程图如下:
AQS原理分析_第4张图片

从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程):

AQS原理分析_第5张图片

acquireQueued()方法中的Finally代码:
通过cancelAcquire()方法,将Node的状态标记为CANCELLED

private void cancelAcquire(Node node) {
     
  // 将无效节点过滤
	if (node == null)
		return;
  // 设置该节点不关联任何线程,也就是虚节点
	node.thread = null;
	Node pred = node.prev;
  // 通过前驱节点,跳过取消状态的node
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;
  // 获取过滤后的前驱节点的后继节点
	Node predNext = pred.next;
  // 把当前node的状态设置为CANCELLED
	node.waitStatus = Node.CANCELLED;
  // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
	if (node == tail && compareAndSetTail(node, pred)) {
     
		compareAndSetNext(pred, predNext, null);
	} else {
     
		int ws;
    // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
    // 如果1和2中有一个为true,再判断当前节点的线程是否为null
    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
		if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
     
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				compareAndSetNext(pred, predNext, next);
		} else {
     
      // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
			unparkSuccessor(node);
		}
		node.next = node; // help GC
	}
}

当前的流程:

获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的pard节点和当前Node关联,将当前Node设置为CANCELLED。

根据当前节点的位置,考虑以下三种情况:

  1. 当前节点是尾节点,设置前驱节点尾结点为null;
  2. 当前节点是Head的后继节点,设置当前节点下一个节点为null
  3. 当前节点不是Head的后继节点,也不是尾节点,将前驱节点的的后继节点设置为下一个节点

到此,lock()方法执行结束!

下面是unlock()解锁方法

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

		// 当前线程释放锁
		public final boolean release(int arg) {
     
		    if (tryRelease(arg)) {
     
		        Node h = head;
		        if (h != null && h.waitStatus != 0) 
		        // 若队列非空或者当前线程不处于唤醒状态,唤醒当前线程后面的一个线程
		            unparkSuccessor(h);
		        return true;
		    }
		    return false;
		}

这里的判断条件为什么是h != null && h.waitStatus != 0?

h == null Head还没初始化。
初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。
所以说,这里如果还没来得及入队,就会出现head == null 的情况。

h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。

h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

		// 方法返回当前锁是不是没有被线程持有
		protected final boolean tryRelease(int releases) {
     
			// 减少可重入次数
			int c = getState() - releases;
			// 当前线程不是持有锁的线程,抛出异常
			if (Thread.currentThread() != getExclusiveOwnerThread())
				throw new IllegalMonitorStateException();
			boolean free = false;
			// 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state
			if (c == 0) {
     
				free = true;
				setExclusiveOwnerThread(null);
			}
			setState(c);
			return free;
		}
		private void unparkSuccessor(Node node) {
     
			// 获取头结点waitStatus
			int ws = node.waitStatus;
			if (ws < 0)
				compareAndSetWaitStatus(node, ws, 0);
			// 获取当前节点的下一个节点
			Node s = node.next;
			// 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
			if (s == null || s.waitStatus > 0) {
     
				s = null;
				// 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
				for (Node t = tail; t != null && t != node; t = t.prev)
					if (t.waitStatus <= 0)
						s = t;
			}
			// 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
			if (s != null)
				LockSupport.unpark(s.thread);
		}

为什么要从后往前找第一个非Cancelled的节点呢?

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

我们从addWaiter()方法中可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

在ReentrantLock中

加锁:

  • 通过ReentrantLock的加锁方法Lock进行加锁操作。

  • 会调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的Acquire方法。

  • AQS的Acquire方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。

  • tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

解锁:

  • 通过ReentrantLock的解锁方法Unlock进行解锁。

  • Unlock会调用内部类Sync的Release方法,该方法继承于AQS。

  • Release中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。

  • 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。

通过上面的描述,大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系。

总结

AQS在JUC中的应用

同步工具 同步工具与AQS的关联
ReentrantLock 使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。
Semaphore 使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
CountDownLatch 使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。
ReentrantReadWriteLock 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。
ThreadPoolExecutor Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。

参考文章:

从ReentrantLock的实现看AQS的原理及应用

你可能感兴趣的:(知识总结,java)