AQS源码解析

227 阅读16分钟

1. 什么是AQS?

AQS(AbstractQueuedSynchronizer)是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AQS实现的。

AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。

2. AQS解决了什么问题?

ReentrantLock为例(ReentrantLock是可重入锁,指的是一个线程能够对一个临界资源重复加锁),ReentrantLock支持公平锁和非公平锁,其中非公平锁源码中的加锁流程如下:

// java.util.concurrent.locks.ReentrantLock#NonfairSync

// 非公平锁
static final class NonfairSync extends Sync {
	final void lock() {
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
			acquire(1);
		}
}
// 公平锁
static final class FairSync extends Sync {
	final void lock() {
		acquire(1);
	}
}

可以看出非公平锁加锁部分代码的含义为:

  • 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
  • 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。 获取锁成功很好理解,那么获取锁失败后应该怎么处理呢?无非有两种情况:
  • (1)将当前线程获锁结果设置为失败,获取锁流程结束。
  • (2)存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 第一种设计会极大降低系统的并发度,显然不适合实际使用。那么采用第二种仍需解决以下问题:
  • 排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
  • 处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
  • 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗? 结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire方法,而Acquire方法是FairSyncUnfairSync父类AQS中的核心方法。可以说,AQS就是用来解决上述问题的。

3. AQS原理概述

AQS的核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。

这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH:Craig、Landin and Hagersten队列,由三位大牛的名字组成,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 image.png AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

AQS源码如下: image.png

(1)数据结构

AQS中最基本的数据结构——NodeNode即为上面CLH变体队列中的节点。

static final class Node {
        //表示线程以共享的模式等待锁
        static final Node SHARED = new Node();
        //表示线程以独占的模式等待锁
        static final Node EXCLUSIVE = null;
        //表示处于该节点的线程
        volatile Thread thread;
        
        //下面4种状态都是waitStatus的枚举值,0是当一个Node被初始化的时候的默认值
        //为1,表示线程获取锁的请求已经取消了
        static final int CANCELLED =  1;
        //为-1,表示线程已经准备好了,就等资源释放了
        static final int SIGNAL  = -1;
        //为-2,表示节点在等待队列中,节点线程等待唤醒
        static final int CONDITION = -2;
        //为-3,当前线程处在SHARED情况下,该字段才会使用
        static final int PROPAGATE = -3;
        
        //当前节点在队列中的状态,即上面几种状态
        volatile int waitStatus;
        
        //前驱指针
        volatile Node prev;
        //后继指针
        volatile Node next;
    }

(2)同步状态State

AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。

private volatile int state;

//以下方法都是Final修饰的,说明子类中无法重写它们

//获取State的值
protected final int getState() {
        return state;
    }
    
//设置State的值
protected final void setState(int newState) {
        state = newState;
    }
    
//使用CAS方式更新State
protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

state有两种状态

  • 0:没有线程占用
  • 大于等于1:有线程占用,大于1时表示可重入锁 我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。 image.png image.png

4. AQS重要方法与ReentrantLock的关联

从源码中可以得知,AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。

    //该线程是否正在独占资源。只有用到 Condition才需要去实现它。
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }
    
    //独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    //独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    //共享方式。arg为获取锁的次数,尝试获取资源。
    //负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    //共享方式。arg为释放锁的次数,尝试释放资源。
    //如果释放后允许唤醒后续等待结点返回True,否则返回False。
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。

以非公平锁为例,基本流程如下: image.png image.png

加锁与解锁流程: image.png

加锁

  • 通过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层核心方法的映射关系 image.png

5. AQS加锁流程源码分析

public class AQSDemo {
    public static void main(String[] args) {
        //3个线程模拟来模拟AQS如何进行线程的管理和通知唤醒机制
        ReentrantLock lock = new ReentrantLock();
        
        //A线程是第一个线程,直接获取锁资源
        new Thread(() -> {
                lock.lock();
                try{
                    System.out.println("-----A thread come in");

                    try { 
                        TimeUnit.MINUTES.sleep(20); 
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }finally {
                    lock.unlock();
                }
        },"A").start();

        //由于只能一个线程持有锁,第二个线程B此时只能等待
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("-----B thread come in");
            }finally {
                lock.unlock();
            }
        },"B").start();

        //由于只能一个线程持有锁,第三个线程C此时只能等待
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("-----C thread come in");
            }finally {
                lock.unlock();
            }
        },"C").start();
    }
}

(1)lock()方法

ReentrantLock的非公平锁lock方法源码如下:

static final class NonfairSync extends Sync {
       
        final void lock() {
            if (compareAndSetState(0, 1))
                //第一个线程抢占时,获取锁成功,将当前线程设置为独占线程。
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //第二个及后续线程抢占时,获取锁失败,则进入Acquire方法进行后续处理
                acquire(1);
        }
    }

(2)acquire()方法

当执行Acquire(1)时,会通过tryAcquire获取锁。在这种情况下,如果获取锁失败,就会调用addWaiter加入到等待队列中去。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }
tryAcquire()方法
protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}

可以看出,这里只是AQS的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的。如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。

以下为ReentrantLock非公平锁的tryAcquire实现:

final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取当前状态
            int c = getState();
            //状态为0,没有被占用
            if (c == 0) {
                //cas操作尝试获取锁
                if (compareAndSetState(0, acquires)) {
                    //获取成功,将当前线程设置为独占线程
                    setExclusiveOwnerThread(current);
                    //返回true
                    return true;
                }
            }
            //判断当前线程是否已经是获取锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //是,则state+1,此处体现可重入性
                int nextc = c + acquires;
                if (nextc < 0) 
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //获取锁失败,返回false,进行后续处理
            return false;
        }
addWaiter()方法

获取锁失败后,会执行addWaiter(Node.EXCLUSIVE)加入等待队列。该方法会把对应的线程以Node的数据结构形式加入到双端队列里,返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。

addWaiter()具体实现方法如下:

private Node addWaiter(Node mode) {
        //通过当前的线程和锁模式新建一个节点
        Node node = new Node(Thread.currentThread(), mode);
        //Pred指针指向尾节点Tail
        Node pred = tail;
        if (pred != null) {
            //将新建Node的Prev指针指向Pred
            node.prev = pred;
            //通过compareAndSetTail方法,保证原子操作,完成尾节点的设置
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //节点入队方法
        enq(node);
        return node;
    }
enq()方法
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;
                }
            }
        }
    }

tips:该方法初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点,为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

(3)acquireQueued()方法

acquireQueued方法可以对排队中的线程进行“获锁”操作。一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。

acquireQueued方法流程如下: image.png 源码如下:

final boolean acquireQueued(final Node node, int arg) {
        //标记是否成功拿到资源
        boolean failed = true;
        try {
            //标记等待过程中是否中断过
            boolean interrupted = false;
            //开始自旋,要么获取锁,要么中断
            for (;;) {
                //获取当前节点的前驱节点
                final Node p = node.predecessor();
                /**
                 * 如果p是头结点,说明当前节点在真实数据队列的首部,
                 * 就尝试获取锁(别忘了头结点是虚节点)
                **/
                if (p == head && tryAcquire(arg)) {
                    //获取锁成功,头指针移动到当前node,代码在下面
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                /**
                 * 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了),
                 * 或者是p不为头结点,这个时候就要判断当前node是否要被阻塞,
                 * 被阻塞条件:前驱节点的waitStatus为-1,
                 * 防止无限循环浪费资源
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//挂起线程,代码在下面
                    interrupted = true;
            }
        } finally {
            if (failed)
                //将Node的状态标记为CANCELLED,下面会介绍
                cancelAcquire(node);
        }
    }
setHead方法:把当前节点置为虚节点
private void setHead(Node node) {
	head = node;
	node.thread = null;
	node.prev = null;
}
parkAndCheckInterrupt方法:挂起当前线程,阻塞调用栈,返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
shouldParkAfterFailedAcquire方法:根据前驱节点判断判断当前线程是否应该被阻塞

为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取头结点的节点状态
	int ws = pred.waitStatus;
	// 说明头结点处于唤醒状态
	if (ws == Node.SIGNAL)
		return true; 
	// 通过枚举值我们知道waitStatus>0是取消状态
	if (ws > 0) {
            do {
                 //循环向前查找取消节点,把取消节点从队列中剔除
                 node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                    pred.next = node;
	} else {
            // 设置前任节点等待状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

shouldParkAfterFailedAcquire方法流程如下: image.png

至此何时出队列以及如何出队列的问题解决了,那么又有新问题了:

  • shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1?
  • 是在什么时间释放节点通知到被挂起的线程呢?
(4)cancelAcquire()方法

通过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的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
  • 根据当前节点的位置,考虑以下三种情况:
a. 当前节点是尾节点

当前节点的前一个有效节点设为尾结点 image.png

b. 当前节点是Head的后继节点

当前节点的后一个有效节点设为尾结点 image.png

c. 当前节点不是Head的后继节点,也不是尾节点

当前节点的前一个有效节点与后一个有效节点连接 image.png

tips:为什么cancelAcquire方法所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?

执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。

do {
    node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

6. AQS解锁流程源码分析

依旧以ReentrantLock的unlock()为例,在解锁的时候,并不区分公平锁和非公平锁。

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

进入该方法内部,可以看到本质释放锁的地方,是通过AQS框架来完成的

public final boolean release(int arg) {
        //自定义的tryRelease如果返回true,说明该锁没有被任何线程持有
        if (tryRelease(arg)) {
            //获取头结点
            Node h = head;
            //头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tips:判断条件为什么是h != null && h.waitStatus != 0?

  • h == null Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。
  • h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
  • h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

(1)tryRelease()方法

在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制:

// java.util.concurrent.locks.ReentrantLock.Sync

// 方法返回当前锁是不是没有被线程持有
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;
}

(2)unparkSuccessor()方法

解除线程挂起状态

// java.util.concurrent.locks.AbstractQueuedSynchronizer

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

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

如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。

(3)唤醒线程后的执行流程

线程被唤醒后,会执行return Thread.interrupted(),这个函数返回的是当前执行线程的中断状态,并清除。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private final boolean parkAndCheckInterrupt() {
        //线程挂起,线程不会继续向下执行
	LockSupport.park(this);
         //根据park方法API的描述,程序在下述三种情况会继续向下执行
        //1.被unpark
        //2.被中断(interrupt)
        //3.其他不合逻辑的返回的返回才会继续向下执行
        
        //因上述三种情况,程序执行至此,返回当前线程的中断状态,并清空中断状态
        //如果由于被中断,该方法返回true
	return Thread.interrupted();
}

还记得对排队中的线程进行“获锁”操作的方法acquireQueued吗?

parkAndCheckInterrupt返回True或者False的时候,interrupted的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前interrupted返回。 image.png

如果acquireQueued为True,就会执行selfInterrupt方法,该方法是为了中断线程(这部分属于Java提供的协作式中断知识内容)。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    static void selfInterrupt() {
	Thread.currentThread().interrupt();
    }
  • 线程中断可以在线程内部设置一个中断标识,同时让处于(可中断)阻塞的线程抛出InterruptedException中断异常,使线程跳出阻塞状态。
  • 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。
  • 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。

(4)小结

  • 某个线程获取锁失败的后续流程是什么呢? 答:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。
  • 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? 答:是CLH变体的FIFO双端队列。
  • 如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题? 答:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放。
  • Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢? 答:AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现,通过tryAcquire完成加锁过程。

7. AQS的应用

(1)ReentrantLock的可重入应用

ReentrantLock的可重入性是AQS很好的应用之一,在了解完上述知识点以后,我们很容易得知ReentrantLock实现可重入的方法。在ReentrantLock里面,不管是公平锁还是非公平锁,都有一段逻辑。

//公平锁
// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
if (c == 0) {
	if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
		setExclusiveOwnerThread(current);
		return true;
	}
}
else if (current == getExclusiveOwnerThread()) {
	int nextc = c + acquires;
	if (nextc < 0)
		throw new Error("Maximum lock count exceeded");
	setState(nextc);
	return true;
}
//非公平锁
// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
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;
}

从上面这两段都可以看到,有一个同步状态State来控制整体可重入的情况。State是Volatile修饰的,用于保证一定的可见性和有序性。

  • State初始化的时候为0,表示没有任何线程持有锁。
  • 当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁是,就会多次+1,这里就是可重入的概念。
  • 解锁也是对这个字段-1,一直到0,此线程对锁释放。

(2)JUC中的应用场景

除了上边ReentrantLock的可重入性的应用,AQS作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具,大体介绍一下AQS的应用场景: image.png

(3)自定义同步工具

了解AQS基本原理以后,按照上面所说的AQS知识点,自己实现一个同步工具。

public class LeeLock  {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire (int arg) {
            return compareAndSetState(0, 1);
        }

        @Override
        protected boolean tryRelease (int arg) {
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively () {
            return getState() == 1;
        }
    }
    
    private Sync sync = new Sync();
    
    public void lock () {
        sync.acquire(1);
    }
    
    public void unlock () {
        sync.release(1);
    }
}

通过我们自己定义的Lock完成一定的同步功能。

public class LeeMain {

    static int count = 0;
    static LeeLock leeLock = new LeeLock();

    public static void main (String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run () {
                try {
                    leeLock.lock();
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    leeLock.unlock();
                }

            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

上述代码每次运行结果都会是20000。通过简单的几行代码就能实现同步功能,这就是AQS的强大之处。

本文参考自美团技术团队的文章:从ReentrantLock的实现看AQS的原理及应用
文章链接tech.meituan.com/2019/12/05/…