并发编程之AQS详解

Java并发编程核心在于java.util.concurrent包(简称juc)。

这整个包的作者都是:doug lea

什么是AQS

AQS 是 AbstractQueuedSynchronizer 的简称。AbstractQueuedSynchronizer 是一个抽象类。

虽然不会直接使用这个类,但是这个类是Java很多并发工具的底层实现。在 juc 中,常用的那些类(例如ReentrantLock、CountDownLatch、Semaphore),内部都维护了一个Sync属性(同步器),而 Sync 就是一个继承了 AbstractQueuedSynchronizer 的内部类。

AQS定义了两种资源共享方式:

  1. Exclusive-独占,只有一个线程能执行,如ReentrantLock
  2. Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

节点Node与关键属性

AQS中定义的Node

AQS内部维护了一个双向链表,这个双向链表的元素用Node表示。

Node就定义在AbstractQueuedSynchronizer中:

static final class Node {
	// 标记节点未共享模式
	static final Node SHARED = new Node();
	// 标记节点为独占模式
	static final Node EXCLUSIVE = null;
	// 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
	static final int CANCELLED = 1;
	/**
	 *  后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
	 *  将会通知后继节点,使后继节点的线程得以运行。
	 */
	static final int SIGNAL = -1;
	/**
	 *  节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
	 *  该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
	 */
	static final int CONDITION = -2;
	// 表示下一次共享式同步状态获取将会被无条件地传播下去
	static final int PROPAGATE = -3;
	/**
	 * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
	 * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,被一个线程修改后,状态会立马让其他线程可见。
	 */
	volatile int waitStatus;
	// 前驱节点,当前节点加入到同步队列中被设置
	volatile Node prev;
	// 后继节点
	volatile Node next;
	// 节点同步状态的线程
	volatile Thread thread;
	/**
	 * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
	 * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
	 */
	Node nextWaiter;
	// 如果节点是共享状态,返回true
	final boolean isShared() {
		return nextWaiter == SHARED;
	}
	// 返回前驱节点
	final Node predecessor() throws NullPointerException {
		Node p = prev;
		if (p == null)
			throw new NullPointerException();
		else
			return p;
	}
	Node() {    // Used to establish initial head or SHARED marker
	}
	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;
	}
}

AQS关键属性

// 指向同步等待队列的头节点
private transient volatile Node head;

// 指向同步等待队列的尾节点
private transient volatile Node tail;

// 同步资源状态。例如在ReentrantLock中,state标识加锁的次数
private volatile int state;

公平锁&非公平锁

公平锁

公平锁中,如果已经有线程在等待,则线程会直接进入等待队列。等待队列中除第一个线程以外,所有线程都会别阻塞,需要CPU去唤醒。

只有在当前没有线程在等待时,才会去获取锁:

优点是所有的线程都能得到资源;缺点是队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁

非公平锁中,线程尝试获取锁时,即使此时已经有其他线程在等待队列中,该线程也会直接去尝试获取锁(compareAndSetState)。

通过代码也也可以看到,加锁的时候直接通过 cas 尝试获取锁:

非公平锁的优点是,如果线程直接获取到了锁,就不用加到等待线程中了,可以减少CPU唤醒线程的开销,所以性能比公平锁高;缺点顾名思义,不公平!可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁。

在代码中则是:FairSync(公平锁) 与 NonfairSync(非公平锁),ReentrantLock 默认是"非公平锁"。

在 ReentrantLock 有一个构造方法就可以传入是否是公平锁:

可重入锁&不可重入锁

可重入指的是可以重复加锁,否则是不可重入。

可重入锁

可重入锁指的是:以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁(其他的线程是不可以的)。

ReentrantLock 就是一个典型的可重入锁。来看一个示例:

state 用于存储加锁的次数。每加一次锁,state值加1;每解锁一次,state值减1;

不可重入锁

当一个线程获取对象锁之后,这个线程不能再次获取该锁,必须先释放锁后才能重新获取锁。

JDK中没有不可重入锁的实现。

等待队列

CLH等待队列,是一个双向链表,是根据 Node 类构建的。每次新加节点,会加在队列的末尾。

当一个线程进来,需要等待时,会加入CLH等待队列中。在实现上是创建一个 Node 对象,加入到双向链表中。

 在代码中来看,是通过 Node 的 prev 和 next 数量连接的。

条件队列

某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁。

条件队列也是根据 Node 类构建的,不过跟CLH等待队列不同,它是一个单向链表

条件队列中,Node 的 waitStatus = Node.CONDITION = -2。且 Node 之间的连接,通过 nextWaiter,而不是 prev 和 next。

在操作条件队列之前首先需要成功获取独占锁,成功获取独占锁以后,如果当前条件还不满足,则在当前锁的条件队列上挂起,与此同时释放掉当前获取的锁资源,条件满足后的再唤醒。

BlockingQueue就是通过条件队列来实现的。

具体不展开了,条件队列在AQS中,相对其他功能来说比较独立,后面单独写一篇文章。

独占方式

独占方式就是:一个线程拿到了锁,其它线程就不能再拿锁了。

通过 Node.EXCLUSIVE 表示独占模式

AbstractOwnableSynchronizer 继承了抽象类 AbstractOwnableSynchronizer

在这个类中,定义了一个变量 exclusiveOwnerThread,它表示独占模式下,同步器是由哪个线程持有的。

1)获取锁

以 ReentrantLock 为例,它就是独占模式的。

首先第一个线程通过CAS获取锁,然后设置 exclusiveOwnerThread 为当前线程:

2)加入阻塞队列

当别的线程想要获取锁时,如果发现锁已经被别的线程持有了,则不能加锁成功:

如果加锁失败,正常情况下会加入等待列表中并阻塞。

addWaiter方法用于创建Node节点并将它放到等待队列尾部:

private Node addWaiter(Node mode) {
	// 将当前线程构建成Node类型
	Node node = new Node(Thread.currentThread(), mode);
	Node pred = tail;
	// 当前tail节点是否为null?
	if (pred != null) {
		node.prev = pred;
		// CAS将节点插入同步队列的尾部,然后return当前节点
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	// 当前tail节点为空,说明等待队列为空。此时创建等待队列,并将Node添加到等待队列尾部
	enq(node);
	return node;
}

acquireQueued方法用于在没有获取锁时,阻塞当前线程,阻塞前还是会再尝试下获取锁:

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
		    // 如果prev节点是head节点,则再尝试获取锁
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			if (
				/**
				 * 判断在获取锁失败后,是否需要阻塞
				 * 内部会设置为SIGNAL状态,所以很快会返回true,然后就能阻塞线程了
				 * 所以不会让这个死循环一直跑下去占用CPU
				 */
			    shouldParkAfterFailedAcquire(p, node) &&
				// 将当前线程阻塞
				parkAndCheckInterrupt()
			)
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

看一个例子:

public class ExclusiveTest {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println("线程1加锁成功");
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
            System.out.println("线程1解锁完毕");
        });

        Thread t2 = new Thread(() -> {
            lock.lock();
            System.out.println("线程2加锁成功");
            lock.unlock();
            System.out.println("线程2解锁完毕");
        });

        t1.start();
        Thread.sleep(500);
        t2.start();
        System.out.println("==============");
    }
}

启动两个线程,线程t1(Thread-0)获取了锁,但是一直没有释放。线程2(Thread-1)再想去获取锁会失败,会加入等待列表中。

3)持有锁的线程释放锁后,唤醒等待队列中的线程

当持有锁的线程将锁释放,会唤醒处于等待队列中的线程。

当调用 unlock() 方法释放锁时,

/**
 * 释放独占模式持有的锁
 */
public final boolean release(int arg) {
	// tryRelease(arg):arg = 1,释放一次锁,state - 1
	if (tryRelease(arg)) { 
		Node h = head;
		// 如果head节点处于SIGNAL状态(waitStatus = -1),说明有需要唤醒的线程
		if (h != null && h.waitStatus != 0)
		// 唤醒后继(next)结点中的线程
		unparkSuccessor(h);
		return true;
	}
	return false;
}

首先,持有锁的线程尝试释放锁(tryRelease),在它的内部会把(state-1),也就是释放“一次”锁,只有当state减到0,此线程才会最终释放锁,返回true,否则返回false

protected final boolean tryRelease(int releases) {
	// state 减 1
	int c = getState() - releases;
	// 如果持有锁的不是当前线程,则抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		// 如果state已经减到0了,说明此线程已经彻底释放锁了
		free = true;
		// 由于当前线程已经释放锁,所以exclusiveOwnerThread置空
		setExclusiveOwnerThread(null);
	}
	// 设置最新的state值
	setState(c);
	return free;
}

如果线程成功释放锁,则看head节点是否处于SIGNAL状态(waitStatus=-1),则唤醒等待队列中的线程(head的后继节点),具体实现在unparkSuccessor()方法中:

/**
 * 唤醒节点对应的线程。正常情况下,唤醒的是head的后继节点中的线程
 */
private void unparkSuccessor(Node node) {
	// 此node是head节点
	int ws = node.waitStatus;
	if (ws < 0)
		// 将等待状态waitStatus设置为初始值0。其实最普通的处于等待队列中的节点的waitStatus就是0
		compareAndSetWaitStatus(node, ws, 0); 
	// head的后继节点
	Node s = node.next;
	// 若后继结点为空,或状态为已失效(CANCEL=1)
	if (s == null || s.waitStatus > 0) {
		s = null;
		// 从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点进行唤醒
		for (Node t = tail; t != null && t != node; t = t.prev)
			if (t.waitStatus <= 0)
				s = t;
	}
	if (s != null)
		// 调用本地(native)方法唤醒线程
		LockSupport.unpark(s.thread);
}

线程被唤醒后,就能去获取锁了。

共享方式

共享方式就是:支持多个线程同时获取锁,访问共享资源。

以 Semaphore 为例(默认也是创建非公平锁)。Semaphore 的作用是限流。在 Semaphore 中,state 表示的是资源总数。

例如在同一时刻只允许2个线程能访问资源,那么 state 初始就是 2。

举个例子:

public class SemaphoreTest {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 1; i < 6; i++) {
            Thread thread = new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "加锁成功==>" + System.currentTimeMillis());
                    Thread.sleep(5000);
                    semaphore.release();
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "解锁完毕==>" + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "线程" + i);
            thread.start();
        }
    }
}

起5个线程,线程中睡眠5秒,可以看到现象是一开始只能有2个线程获取锁,5秒时间到了之后,等有线程释放锁之后,其他线程才能继续获得锁。但是总共只能有2个线程能同时获取锁。

接下来看下源码中怎么实现的。

1)获取锁

如果能获取锁,就正常返回。不抛异常,也不阻塞。 

public final void acquireSharedInterruptibly(int arg)
		throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
	// 尝试获取共享锁,如果tryAcquireShared返回>=0,则说明获取成功了
	if (tryAcquireShared(arg) < 0)
	    // 如果没有获取到锁,则正常来讲该线程应该加入等待队列
		doAcquireSharedInterruptibly(arg);
}

判断是否能获取到共享锁:

final int nonfairTryAcquireShared(int acquires) {
	for (;;) {
	    // state,初始值是是构造函数中传入的permits,也就是同时支持的线程数
		int available = getState();
		// 剩余的可执行线程数 = state - 1
		int remaining = available - acquires;
		// 如果remaining<0,说明锁已经都被占用了。否则cas修改state值
		if (remaining < 0 || compareAndSetState(available, remaining))
			return remaining;
	}
}

2)如果获取不到锁,将当前线程加入等待队列,并阻塞

如果没有获取到锁(返回的remaining小于0),一般情况下会将它放到等待队列中,并阻塞当前线程。

private void doAcquireSharedInterruptibly(int arg)
	throws InterruptedException {
	// 创建Node节点,并加入等待队列
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true;
	try {
		for (;;) {
		    // prev节点
			final Node p = node.predecessor();
			if (p == head) {
			    // 如果当前Node的prev节点是head,则再尝试下是否能获取锁
				int r = tryAcquireShared(arg);
				if (r >= 0) {
					setHeadAndPropagate(node, r);
					p.next = null; // help GC
					failed = false;
					return;
				}
			}
			if (
			    /**
				 * 判断在获取锁失败后,是否需要阻塞
				 * 内部会设置为SIGNAL状态,所以很快会返回true,然后就能阻塞线程了
				 * 所以不会让这个死循环一直跑下去占用CPU
				 */
			    shouldParkAfterFailedAcquire(p, node) &&
			    // 调用UNSAFE的方法,将线程阻塞
				parkAndCheckInterrupt()
			)
				throw new InterruptedException();
		}
	} finally {
		if (failed)
		    // 取消获取锁
			cancelAcquire(node);
	}
}

addWaiter方法用于创建Node节点并将它放到等待队列尾部。源码在独占模式中解释过了。

enq方法用于在等待队列为空时,创建等待队列,并将Node添加到等待队列尾部:

private Node enq(final Node node) {
	// 死循环,确保节点成功添加到等待列表尾部,没有列表的话就创建列表
	for (;;) {
		Node t = tail;
		if (t == null) {
			// 队列为空需要初始化,创建空的头节点head和尾结点tail
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			// 队列不为空之后,把当前Node加到列表尾部
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}

shouldParkAfterFailedAcquire方法用于判断在获取锁失败后,是否需要阻塞:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 若prev节点的状态是SIGNAL,意味着当前结点可以被安全地park
		return true;
	if (ws > 0) {
		// 如果prev节点状态为取消状态(waitStatus=1),则移出队列
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {
		// 如果prev节点为其他状态,则将它设置为SIGNAL状态,是它可以被安全地park
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

shouldParkAfterFailedAcquire方法内部会设置为SIGNAL状态,所以很快会返回true。然后就会调用parkAndCheckInterrupt()将当前线程阻塞了。所以不会一直自旋占用CPU!

3)线程释放锁

当线程使用公共资源结束时,会把锁释放回去。state 值会加回去。

首先检查是否能释放一次(releases=1)共享锁,如果能的话,state先加1,再返回true:

protected final boolean tryReleaseShared(int releases) {
	for (;;) {
	    // 获取当前的state值
		int current = getState();
	    // releases = 1,
		int next = current + releases;
		if (next < current)
			throw new Error("Maximum permit count exceeded");
		// 通过cas方式更新state,state += 1
		if (compareAndSetState(current, next))
			return true;
	}
}

tryReleaseShared 返回 true,说明可以释放锁,则开始唤醒等待队列中的线程:

/**
 * 把当前结点设置为SIGNAL或者PROPAGATE
 * 唤醒head.next(B节点),B节点唤醒后可以竞争锁,成功后head->B.next,然后又会唤醒B.next,一直重复直到共享节点都唤醒
 * head节点状态为SIGNAL,重置head.waitStatus->0,唤醒head节点线程,唤醒后线程去竞争共享锁
 * head节点状态为0,将head.waitStatus->Node.PROPAGATE传播状态,表示需要将状态向后继节点传播
 */
private void doReleaseShared() {
	for (;;) {
		Node h = head;
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			// head是SIGNAL状态,说明可以唤醒后面的节点线程
			if (ws == Node.SIGNAL) {
				/**
				 * head状态是SIGNAL,重置head节点waitStatus为0,这里不直接设为Node.PROPAGATE,
				 * 是因为unparkSuccessor(h)中,如果ws<0会设置为0,所以ws先设置为0,再设置为Node.PROPAGATE
				 */
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					// waitStatus设置0失败的话,重新循环
					continue;
				/**
				 * 调用native方法唤醒head.next节点线程,唤醒后的线程会去竞争锁
				 * 成功后head会指向下下个节点,也就是head发生了变化
				 */
				unparkSuccessor(h);
			}
			/**
			 * 如果本身head节点的waitStatus是处于重置状态(waitStatus==0)的,将其设置为“传播”状态。
			 * 意味着需要将状态向后一个节点传播
			 */
			else if (ws == 0 &&
					!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;
		}
		// 如果head变了,重新循环,继续唤醒head的下一个节点
		if (h == head)
			break;
	}
}

注意,会唤醒等待队列中的所有节点线程,然后他们回去竞争锁。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值