AQS框架学习

0 AQS简介

0.1 核心思想

抽象队列同步器实现了锁机制,可以实现共享锁和互斥锁;在某一工作线程想要持有该锁时,若该锁能够被其持有(资源空闲或共享锁),那么AQS会将该线程维护为有效的工作线程;若该锁不能被其持有(资源非空闲),AQS会利用其实现的一套线程等待及被唤醒时的锁分配机制去维护这个线程

在AQS的实现上,主要用了以下几个点:

  • CLH队列,维护获取共享资源失败的阻塞线程
  • 自旋锁,与cas配合,更新AQS中维护的属性
  • cas,与自旋锁配合,更新AQS中维护的属性
  • LockSupport的静态方法park()和unpark(),实现线程的阻塞和唤醒

0.2 AQS几个关键属性

// 维护线程的节点,队列中维护的就是Node
static final class Node {
	...
	// 用于waitStatus,这里只列举了学习到的
	// 指示当前Node的后继需要unpart()操作
	/** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL = -1;

	// 节点状态,初始为0
	volatile int waitStatus;

	// 指向队列前一个节点
	volatile Node prev;

	// 指向队列后一个节点
	volatile Node next;
	
	// 维护的线程
	volatile Thread thread;
	...
}

// 指向队列的队首,lazily initialized
private transient volatile Node head;

// 指向队列的队尾,lazily initialized
private transient volatile Node tail;

// 队列状态
private volatile int state; 

0.3 模板设计模式

AQS基于模板设计模式,框架实现了线程阻塞队列的维护逻辑,在自定义同步器时,可以根据需要实现如下几个模板方法即可:

// 互斥 获取锁
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
// 互斥 释放锁
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
// 共享 获取锁
protected int tryAcquireShared(int arg) {
	throw new UnsupportedOperationException();
}
// 共享 释放锁
protected boolean tryReleaseShared(int arg) {
	throw new UnsupportedOperationException();
}
// return true if synchronization is held exclusively
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

这里的模板方法都声明为protected,而非抽象方法,意在让继承者选择需要的方法重写即可,而不需要将每个方法都实现,降低了代码的冗余

1 CLH队列

这里参照了https://blog.csdn.net/firebolt100/article/details/82662102进行学习并记录

1.1 SMP(symmetric multi processing)对称多处理器介绍

其指一种包括软硬件的多核计算架构,有两个及两个以上的相同的核心共享一块主存,每个核心在操作系统中的地位相同,可以访问所有的I/O设备
SMP架构

1.2 CLH队列实现

1.2.1 CLH队列锁思想:

CLH队列锁采用主动等待的方式,自旋检测队列前一个元素状态,若检测到状态满足当前元素被唤醒,则结束自旋;自旋实际上是cpu空转,看起来有些浪费性能,但实际上对于小任务而言,空转时间非常短暂,相比于阻塞等待的方式,自旋方式的消耗要比阻塞方式时的线程管理和切换的开销要小

1.2.2 java实现

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {

	/**
	* 每个节点都代表一个申请加锁的线程对象,active属性表示该线程处于活跃状态(true):
	* 1、正在排队等待加锁
	* 2、已经获取到了锁
	* 非活跃状态(false)表示当前节点线程工作完毕,后续节点可以结束自旋并开始工作
	*/
	private static class CLHNode {
		volatile boolean active = true; // 默认活跃
	}

	// 隐式链表尾节点
	private volatile CLHNode tail = null;

	// 绑定当前线程状态
	private ThreadLocal<CLHNode> currentThreadNode = new ThreadLocal<>();

	private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> updater = 
		AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");

	// 加锁
	public void lock() {
		
		// 获取当前线程绑定状态
		CLHNode cNode = currentThreadNode.get();
	
		// 未初始化,则绑定一个默认为活动的线程状态
		if(cNode == null)
			currentThreadNode.set(cNode = new CLHNode());

		// 维护链表,返回前驱节点
		// api返回更新前的值
		CLHNode predecessor = updater.getAndSet(this, cNode);

		// 若有前驱,则需要根据前驱节点的active状态自旋
		if(predecessor != null)
			for(; predecessor.active;) {}

		// 若无前驱节点,表示可以直接获取锁

	}

	// 释放锁
	public void unlock() {

		CLHNode cNode = currentThreadNode.get();
		
		// 当前线程不持有锁或非活跃
		if(cNode == null || !cNode.active)
			return;

		// 移除当前线程的绑定状态
		currentThreadNode.remove();

		/**
		 * 队列中所有活跃的非尾节点,都在lock()方法中由predecessor.active引用,因此
		 * 通过cas对活跃的节点设置null是不会成功的,这是便需要对节点的活跃状态设置为
		 * false,从而使后继节点结束自旋;若是尾巴节点,那么cas操作会成功,而由于是尾
		 * 节点,因此什么都不需要做,返回即可
		 */
		 if(!updater.compareAndSet(this, cNode, null))
		 	cNode.active = false;

	}

	// 生成任务
	private static Runnable generateTask(final CLHLock, final String taskId) {

		return () -> {
			lock.lock();
			try {
				Thread.sleep(3000); // 模拟线程工作3s
			} catch (InterruptedException e) {
				e.printStackTrace();
			}			
			System.err.println(String.format("Thread %s completed.", taskId));
			lock.unlock();
		};

	}

	public static void main(String[] args) {
        final CLHLock lock = new CLHLock();
        for (int i = 0; i < 10; i++)
            new Thread(generateTask(lock, i + "")).start();
    }

}

2 ReentrantLock及AQS应用

通过ReentrantLock,了解AQS独占锁模式工作方式

2.1 ReentrantLock demo

利用java.util.concurrent.locks.ReentrantLock实现同步:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    static ReentrantLock lock = new ReentrantLock(true); // 公平锁

    public static void main(String[] args) {
        // 启动两个线程
        new Thread(ReentrantLockDemo::testSync, "t1").start();
        new Thread(ReentrantLockDemo::testSync, "t2").start();
    }

    public static void testSync() {
        // 加锁
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " running ");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " over ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 在finally块中,保证发生异常时也能释放锁
            lock.unlock();
        }
    }

}

2.2 lock()方法源码分析

ReentrantLock中维护了继承自AQS的抽象静态内部类Sync,有分别有继承自ReentrantLock.Sync的静态内部类FairSyncNonfairSync来实现公平锁和非公平锁;上述demo使用的是公平锁,但这里会将公平锁和非公平锁一并列出比对,在实现上只有部分处理不同,大部分是相同的

当执行了lock.lock()后,会调用ReentrantLock.lock(),然后又调用ReentrantLock中Sync.lock()方法:

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

Sync.lock()是抽象方法,分别由FairSyncNonfairSync重写,公平非公平,就体现在下面FairSyncNonfairSynclock()方法的实现方式中;

// Sync中lock()的声明
abstract void lock();

// FairSync中的lock
final void lock() {	
	// 根据AQS中锁状态及队列状态等,尝试获取锁
	acquire(1);
}

// NonfairSync
final void lock() {
	// 不会根据AQS中的队列状态,直接尝试插队获取锁
	if (compareAndSetState(0, 1))
		// 获取成功后修改AQS中独占锁线程属性
    	setExclusiveOwnerThread(Thread.currentThread());
    else
    	// 根据AQS中锁状态及队列状态等,尝试获取锁
        acquire(1);
}

下面是AbstractQueuedSynchronizer.acquire(int arg)的实现,此方法无任何返回,虽然对对interrupt标志位有过处理,但是调用acquire()方法却得不到任何有关interrupt的信息,因此也无法对打断做处理:

public final void acquire(int arg) {
	// 获取资源失败,说明存在竞争
	if (!tryAcquire(arg) &&
    	acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    	// acquireQueued()反回线程interrupt标志位是否被程序复位过,若复位过,
    	// 则进入selfInterrupt()将标志位再修改为用户所操作的标志的结果
    	selfInterrupt();
}

接下来不再列举ReentrantLock.NonfairSync的代码,ReentrantLock.FairSync.tryAcquire()实现,这里的实现体现了ReentrantLock可重入性

protected final boolean tryAcquire(int acquires) {
	// 获取当前线程
	final Thread current = Thread.currentThread();
	// 获取同步器状态
    int c = getState();
    // 资源空闲
    if (c == 0) {
    	// 阻塞队列中是否有前驱节点,这里意为,即使资源空闲,
    	// 也只有同步队列为空或当前节点为head的下一个节点,才
    	// 能获取资源
        if (!hasQueuedPredecessors() && 
        	// 没有前驱节点则cas修改同步器状态
            compareAndSetState(0, acquires)) {
            // 修改成功,设置同步器持有线程
            setExclusiveOwnerThread(current);
            // 获取资源成功
            return true;
        }
    }
    // 资源非空闲,当前线程是同步器的持有者
    // 在这里体现了ReentrantLock的可重入性
    else if (current == getExclusiveOwnerThread()) {
    	// 新状态 = 当前状态 + 本次设置的状态
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置状态
        setState(nextc);
        // 获取资源成功
        return true;
    }
    // 获取资源失败
    return false;
}

AbstractQueuedSynchronizer.hasQueuedPredecessors()代码实现,这里主要用于判断当前线程是否是非head的第一个节点,若是,则有资格获取共享资源:

/**
 * 若有除head外的线程节点先于当前线程入队,返回true
 * 若当前线程节点在除去head外的队列首或队列为空,返回false
 */
public final boolean hasQueuedPredecessors() {
 	// The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    // 队列未初始化,h == t,h和t都为null
    // 队列已初始化,两种情况:
    // 1:队列元素个数大于1,这时候一定存在线程竞争共享资源,h != t一定成立,并且因为队列元素个数大于1,
    // 所以(s = h.next) == null一定为false;而s作为head的下一个节点,若当前线程即为s中的线程,那么说明其
    // 有资格获取锁;从代码来看,s.thread != Thread.currentThread()结果为false,因此整个方法返回false
    //
    // 2:队列元素个数等于1,
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

AbstractQueuedSynchronizer.addWaiter()代码实现,该方法作用是将当前节点放入阻塞队列:

// 在ReentrantLock中调用,传入的mode为Node.EXCLUSIVE
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;
        // cas设置阻塞队列尾节点
        // 这里尝试快速入队,如果失败说明其它线程先入队了,此时就需要在enq()方法中通过自旋尝试入队
        if (compareAndSetTail(pred, node)) {
        	// 设置原尾节点的后继节点
            pred.next = node;
            // 返回新入队的节点
            return node;
        }
    }
    // 入队,来到这个方法,说明当先是存在多线程竞争共享资源的,并且锁被其它线程持有,还没有释放
    enq(node);
    // 返回新入队节点
    return node;
}

AbstractQueuedSynchronizer.enq()代码实现,这里若tail为null,那么必须初始化head,并将tail指向head:

private Node enq(final Node node) {
	// 自旋
	for (;;) {
		// 阻塞队列尾节点
        Node t = tail;
        // 尾节点尾空,必须初始化头节点,并且将tail也指向head
        if (t == null) { // Must initialize
        	// cas初始化head,注意这里是将一个空节点设置为head,也就是doc中描述的dummy header
        	// 存在竞争时,就必须初始化一个空Node并设置为head
            if (compareAndSetHead(new Node()))
            	// tail指向head
                tail = head;
        } else {
        	// 新节点入队
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                // 返回原tail
                return t;
            }
        }
    }
}

AbstractQueuedSynchronizer.acquireQueued()代码实现,该方法作用是,在新节点进入阻塞队列后,处理新节点所存储的线程,若线程能获得资源,则进行工作,否则park()阻塞:

// final Node node, 指新入队的节点
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    	// 是否被interrupt
        boolean interrupted = false;
        // 自旋
        // 当线程竞争资源,没有得到资源的线程将会阻塞在该自旋中,
        // 被唤醒后,仍然在该自旋中尝试获取资源;获取资源成功后,
        // 将当前线程的Node节点设置为head,并清空Node内的属性
        for (;;) {
        	// 当前节点的前驱节点
            final Node p = node.predecessor();
            // 前驱节点为head,表示当前节点有资格竞争资源
            if (p == head && tryAcquire(arg)) {
            	// 竞争资源成功,设置当前节点为head
                setHead(node);
                // 原head断链
                p.next = null; // help GC
                failed = false;
                // 返回
                return interrupted;
            }
            // 判断当前线程获取资源失败后,是否应该进入阻塞状态,即前驱节点的waitStatus是否为Node.Signal
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// 在该方法中调用park()被阻塞,并返回是否被interrupt
            	// 若返回true,也能够说明线程interrupt标志位是被复位
            	// 过的,这个复位操作是非用户操作
                parkAndCheckInterrupt())
                // 代码进行到这里意味着程序修改了用户的行为,
                // 即用户将interrupt标志位修改为true,而程序
                // 将其复位了
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()代码实现:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL) // 前驱节点 waitStatus == Node.SIGNAL 则当前节点应该park()
	    return true;
	if (ws > 0) {
	    do {
	        node.prev = pred = pred.prev;
	    } while (pred.waitStatus > 0);
	    pred.next = node;
	} else {
		// 设置前驱节点waitStatus
	    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

AbstractQueuedSynchronizer.parkAndCheckInterrupt()代码实现:

private final boolean parkAndCheckInterrupt() {
	// 阻塞在当前行,需要其它线程unpark()
    LockSupport.park(this);
    // 判断当前线程是否被interrupt,并复位flag,这个复位操作修改了用户的行为
    return Thread.interrupted();
}

2.3 unlock()方法源码分析

ReentrantLock.unlock()方法直接调用AbstractQueuedSynchronizer.release()方法进行锁的释放,代码如下:

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

AbstractQueuedSynchronizer.release()代码实现:

public final boolean release(int arg) {
	// 释放锁
    if (tryRelease(arg)) {
    	// 获取头节点
        Node h = head;
        // head不为空并且头节点ws不为0
        // head为空说明此前无线程竞争,无线程竞争就不需要初始化head
        if (h != null && h.waitStatus != 0)
        	// 唤醒后继节点线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

ReentrantLock.tryRelease()代码实现如下:

protected final boolean tryRelease(int releases) {
	// ReentrantLock是可重入的,这里用当前state值减去此次释放的state值
    int c = getState() - releases;
    // 当前线程不是同步器持有者,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 标识同步器是否空闲
    boolean free = false;
    // state为0标识共享资源空闲
    if (c == 0) {
        free = true;
        // 清空同步器持有者线程
        setExclusiveOwnerThread(null);
    }
    // 重置state
    setState(c);
    return free;
}

AbstractQueuedSynchronizer.unparkSuccessor()代码实现:

// 这里的入参是同步队列的dummy header
private void unparkSuccessor(Node node) {
   	// head的waitStatus值
    int ws = node.waitStatus;
    // ws小于0则归0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // 正常来说,阻塞队列head的下一个node节点持有即将被唤醒的线程,
    // 但若下一个node节点为null或其状态为Node.CANCELLED,那么就从tail
    // 开始向前遍历,直到找到一个可以被唤醒的线程的Node节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从tail开始寻找合适的Node
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
    	// 唤醒线程
        LockSupport.unpark(s.thread);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章