并发基础_7_并发_锁_队列同步器(AQS)

AbstractQueuedSynchronizer队列同步器(AQS)


废话几句,看AQS之前,最好先了解下设计模式中的 -- "模板模式"

这一节AQS,我花了挺多的时间去看的,看的有些云里雾里的,各位要是暂时看的头晕建议跳过去..

这章我是硬着头皮看下来的,后面还得回炉...

这个队列同步器啊,是Java并发包下的核心之一;

这个基础框架有多重要呢?concurrent包下几乎所有有关锁,多线程以及线程同步重要组件的实现都是基于AQS的..

也就是说,为了后面看并发源码,这个叼玩意必须看明白..



AQS简介

原书在介绍AbstractQueuedSynchronizer的时候,将最重要的一段放在了最后讲,我前面看的一脸懵逼,翻来覆去看了好几遍..


我这里整理下:

AQS是底层实现,AbstractQueuedSynchronizer是抽象类,子类(锁)继承了AQS之后,重写父类的某些方法,实现子类自己的业务(实现)。

原书这样描述:队列同步器的使用方式是继承,子类通过继承同步队列器并实现它的抽象方法来管理同步状态。


队列同步器是实现锁的关键,在锁的实现代码中聚合(结合)同步器,利用同步器实现锁的语义


可以这么理解二者之间的关系:

锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了锁实现的细节

同步器面向的是锁的实现,同步器简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作。

同步器和锁很好的隔离了使用者和实现者所需要关注的领域


下面这张图很好的诠释了同步器和锁之间的关系



AQS的核心思想是基于voliatile int state 这样的一个属性,同时配合Unsafe工具对原子性操作来实现对当前锁的状态进行修改。

当state的值为0时,标识Lock不被任何线程所占有。


从字面意思去看,AbstractQueuedSynchronizer是一个抽象的队列同步器。

抽象:想必有很多方法(模板)还需要开发者去实现;

队列:意味着其中维护这一套遵循FIFO原则的存储结构;

同步:其中蕴含着某种系统级别的同步机制,为线程安全而设计;


原书中是这么介绍AQS的:

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架;
它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
并发包作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。



队列同步器的接口与Demo


同步器的设计是基于模板模式的,也就是说:

a. 使用者需要继承同步器并重写指定的方法;

b. 将同步器组合(插入)在自定义同步组建的实现中;

c. 调用同步器提供的模板方法(父类方法)

d. 而模板方法(父类方法)将会调用子类重写的方法

(模板模式是一种设计模式,我后面会写设计模式方面的内容,不过我肯定,模板模式你肯定用过)


继承同步器之后重写同步器指定的方法时,需要使用下面三个方法来访问或修改同步状态(核心方法)

getState()

获取当前同步状态

setState(int newState)

设置当前同步状态

compareAndSetState(int expect, int update)

使用CAS设置当前状态,该方法能够保证状态设置的原子性


队列同步器可重写的其他主要方法

方法

解释

tryAcquire(int arg)

独占模式下获取对象状态。

此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。

此方法总是由执行 acquire 的线程来调用。

如果此方法报告失败,则 acquire 方法可以将线程加入队列

(如果还没有将它加入队列),直到获得其他某个线程释放了该线程的信号。

tryRelease(int arg)

独占式释放同步状态

试图设置状态来反映独占模式下的一个释放。

此方法总是由正在执行释放的线程调用。

tryAcquireShared(int arg)

共享模式下获取对象状态。

此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它。

此方法总是由执行 acquire 线程来调用。

如果此方法报告失败,则 acquire 方法可以将线程加入队列

(如果还没有将它加入队列),直到获得其他某个线程释放了该线程的信号。

tryReleaseShared(int arg)

共享式释放同步状态

试图设置状态来反映共享模式下的一个释放。

此方法总是由正在执行释放的线程调用。

isHeldExclusively()

查询当前同步器是否在独占模式下被占用

如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true。此方法是在每次调用非等待


......还要其他的不列举了,自己看API 或者去翻书...


同步器提供的模板方法基本上分为三类:

1. 独占式获取与释放同步状态

2. 共享式获取与释放同步状态

3. 查询同步队列中的等待线程情况


自定义同步组件使用同步器提供的模板方法来实现自己的同步语义。

(说白了,就是AQS框架给你提供继承的方法,方法内的具体实现,你自己做,模板模式)


上面的理论看的有点懵逼?没关系~

下面我们来按照书上的Demo整一个,来体会下..(也是官方Demo)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* 0:未锁定<br>
* 1:锁定状态
* 
* @author CYX
*
*/
public class Mute implements Lock {

// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {

	// 当状态为0的是获取锁
	@Override
	protected boolean tryAcquire(int arg) {
	// 如果经过CAS设置状态成功(同步状态设置为1),则代表获取了同步状态
	if (compareAndSetState(0, 1)) {
		setExclusiveOwnerThread(Thread.currentThread());
		return true;
	}
	return false;
	}

	// 释放锁,将状态设置为0
	@Override
	protected boolean tryRelease(int arg) {
	if (getState() == 0)
		throw new IllegalMonitorStateException();

	// 将同步状态重置为0.
	setExclusiveOwnerThread(null);
	setState(0);
	return true;
	}

	// 是否处于独占状态
	@Override
	protected boolean isHeldExclusively() {
	return getState() == 1;
	}

	// 返回一个Condition,每个condition都包含了一个condition队列(暂时不管,后面讲这个Condition是干嘛的.)
	Condition newConditio() {
	return new ConditionObject();
	}

	}

	// 静态内部类继承并重写了AbstractQueuedSynchronizer之后,仅需要将操作代理到Sync上即可
	private final Sync sync = new Sync();

	@Override
	public void lock() {
	sync.acquire(1);
	}

	@Override
	public boolean tryLock() {
	return sync.tryAcquire(1);
	}

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

	@Override
	public Condition newCondition() {
	return sync.newConditio();
	}

	// 不是继承来的,原书中这么写的,官方样例也是这么写的,感觉是对外提供的方法
	public boolean isLocked() {
	return sync.isHeldExclusively();
	}

	// 不是继承来的,原书中这么写的,官方样例也是这么写的,感觉是对外提供的方法
	public boolean hasQueueThreads() {
	return sync.hasQueuedThreads();
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
	sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
	return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

}
上面的这个Demo,独占锁Mutex是一个 自定义同步组件(可以理解为自己实现的一个锁),它在同一时刻只允许一个线程占有锁。

(为什么是独占?因为只实现了tryAcquire(),tryRelease()方法,这两个方法都是独占型的..)


Mutex中定义了一个静态内部类Sync,Sync静态内部类继承了队列同步器,并实现了独占式获取和释放同步状态。

在tryAcquire(int acquires)方法中,如果经过CAS设置状态成功(同步状态为1),则代表获取了同步状态。

而在tryRelease(int releases)方法中只是将同步状态重置为0.


用户在使用Mutex同步组件时,并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法。

在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用队列同步器的模板方法acquire(int args)即可,

当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样子大大降低了实现一个可靠自定义同步组件的门槛。


上面简单的介绍了队列同步器是干嘛的用的,以及写了一个小Demo,大约有了些概念吧...


下面来看看队列同步器是怎么实现的


队列同步器的实现分析

1. 同步队列

AbstractQueuedSynchronizer 队列同步器,从名字就可以看出,AQS内部维护了一个同步队列(FIFO双向队列)来完成同步状态的管理。


如果当前线程获取同步状态失败时,队列同步器会将当前线程以及等待状态等信息构成一个节点(Node)并将其加入同步队列

同时阻塞当前线程,等到同步状态释放,同步器会从同步队列中把首节点中的线程唤醒,使其再次场次获取同步状态。


简单的说,AQS维护的队列就是 当前等待获取资源的队列

我们先来看下静态内部类Node的源码

/**
* Wait queue node class.<br>
* 等待队列节点类(静态内部类)
*
* 队列同步器中的节点Node,用来保存获取同步状态失败的线程的引用、等待状态以及前驱和后继节点<br>
* 这玩意是不是和LinkeHashMap有点类似,前驱节点,后继节点..balabala...<br>
* 
* <pre>
*      +------+  prev +-----+       +-----+
* head |      | <---- |     | <---- |     |  tail
*      +------+       +-----+       +-----+
* </pre>
*/
static final class Node {
	/**
	 * Marker to indicate a node is waiting in shared mode<br>
	 * 表明节点是否以共享模式等待的标记
	 */
	static final Node SHARED = new Node();
	/**
	 * Marker to indicate a node is waiting in exclusive mode<br>
	 * 表明节点是否以独占模式等待的标记
	 */
	static final Node EXCLUSIVE = null;

/**
* waitStatus value to indicate thread has cancelled<br>
* 表明线程已被取消
*/
static final int CANCELLED = 1;
/**
* waitStatus value to indicate successor's thread needs unparking<br>
* 表明后续节点的线程需要unparking
*/
static final int SIGNAL = -1;
/**
* waitStatus value to indicate thread is waiting on condition<br>
* 表明线程正在等待一个状态(条件?)
*/
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate<br>
* 表明下一次acquireShared应该无条件传播
*/
static final int PROPAGATE = -3;

/**
* 等待状态<br>
* <p>
* 包含状态如下<br>
* 1.CANCELLED,值为1。<br>
* 由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态将不会变化<br>
* <p>
* 2.SIGNAL,值为-1。<br>
* 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点线程得以运行<br>
* <p>
* 3.CONDITION,值为-2。<br>
* 节点在等待队列中,节点线程等待在Condition上,当前他线程对Condition调用signal()方法后,<br>
* 该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取<br>
* <p>
* 4.PROPAGATE,值为-3。<br>
* 表示下一次共享式同步状态获取将会无条件的传播下去??<br>
* <p>
* 5INITIAL,值为0。初始状态。<br>
*/
volatile int waitStatus;

/**
* 前驱节点,当节点加入同步队列时被设置(尾部添加)
*/
volatile Node prev;

/**
* 后继节点<br>
*/
volatile Node next;

/**
* 获取同步状态的线程<br>
*/
volatile Thread thread;

/**
* 等待队列中的后继节点。<br>
* 如果当前节点是共享节点,那么这个字段就是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段<br>
*/
Node nextWaiter;

/**
* Returns true if node is waiting in shared mode<br>
* 如果节点在共享模式下等待,则返回true
*/
final boolean isShared() {
	return nextWaiter == SHARED;
}

/**
* Returns previous node, or throws NullPointerException if null. Use
* when predecessor cannot be null. The null check could be elided, but
* is present to help the VM.<br>
* 返回上一个节点,如果为null,则抛出NullPointerException。<br>
* 当前辈不能为null时使用。<br>
* 空检查可能会被消除,但是可以帮助虚拟机。
*
* @return the predecessor of this node
*/
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;
}
}

节点是构成队列同步器的基础

队列同步器中拥有首节点(head) 尾节点(tail)

没有成功获取同步状态的线程将会成为节点,并加入队列的尾部;

同步队列的基本结构如下图:


从图中可以看出,同步队列器中的head首节点是通过引用直接指向首节点,tail尾节点也是通过引用直接指向。


我们幻想一个场景:

当一个线程A成功的获取了同步状态(锁),其他的线程将无法获取到同步状态(锁);
转而被构造成了节点(Node)并加入到了同步队列中,而这个加入同步队列的过程必须是线程安全的;
因此,AQS基于CAS提供了一个设置尾节点的方法:compareAndSetTail(Node expect , Node update),
它需要传递当前线程"认为"的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。


那么问题来了,什么是[当前线程"认为"的尾节点] 和 [当前节点]??(个人见解)

当前线程"认为"的尾节点:就是之前队列的尾节点,老的尾节点。

当前节点:就是要加入同步队列,即将成为同步队列尾节点的那个节点


同步队列遵循FIFO(先进先出),首节点是获取同步状态(锁)成功的节点;

首节点的线程在释放同步状态时,会唤醒后继节点;

后继节点将会在获取同步状态成功时将自己设置为首节点;

过程如下图:

上图中,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能成功获取到同步状态;

因此设置首节点的方法并不需要使用CAS老保证,它只需要将首节点设置为原首节点的后继节点并断开首节点的next引用即可


2. 独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可以获取同步状态(锁),该方法对中断不敏感

也就是说 由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。

下图是获取独占锁的流程图(感谢原作者的图)


原书中的图例



acquire(int arg)方法源码:

/**
* Acquires in exclusive mode, ignoring interrupts. Implemented by invoking
* at least once {@link #tryAcquire}, returning on success. Otherwise the
* thread is queued, possibly repeatedly blocking and unblocking, invoking
* {@link #tryAcquire} until success. This method can be used to implement
* method {@link Lock#lock}.<br>
* 以独占模式获取,忽略中断。 至少一次调用{@link #tryAcquire}来实现,成功返回。
* 否则线程会排队,可能会重复阻塞和解除阻塞,调用{@link #tryAcquire}直到成功。
* 此方法可用于实现方法{@link Lock#lock}。 <br>
* 
*
* @param arg
*            the acquire argument. This value is conveyed to
*            {@link #tryAcquire} but is otherwise uninterpreted and can
*            represent anything you like.
*/
public final void acquire(int arg) {
// 首先尝试获取锁,如果获取失败,会调用addWaiter方法创建节点,并将节点追加到队列尾部。
// 首先调用acquireQueued阻塞或者循环尝试获取锁。
// 在acquireQueued中,如果线程是因为中断而退出的阻塞状态会返回true
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

	// 这里的selfInterrupt主要是为了恢复线程的中断状态。
	selfInterrupt();
	
}
注释写的还算清楚,有必要再解释下:(来自原书)

a. 首先调用自定义同步器实现的tryAcquire()方法,该方法保证线程安全的获取同步状态(锁);

b. 如果获取同步状态(锁)失败,则构造同步节点(Node)(独占式),

c. 然后通过addWaiter()方法将该节点加入到同步队列的尾部。

d. 最后调用acqieureQueued()方法,使得该节点以"死循环"的方式获取同步状态。

e. 如果获取不到阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现


通过上面的描述我们知道acqiure会首先调用tryAcquire()方法来获得锁,该方法需要我们来实现,如果没有获得锁,

会调用addWaiter()方法创建一个和当前线程关联的节点追加到同步队列的尾部,

我们调用addWaiter()方法传入的是Node.EXCLUSIVE,这个常量的意思是:独占模式;


下面是addWaiter()及相关方法的源码:

/**
* Creates and enqueues node for current thread and given mode.<br>
* 为当前线程和给定模式创建和排队节点。
*
* @param mode
*            Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {

// 创建一个Node节点,将当前线程和模式存进去..
Node node = new Node(Thread.currentThread(), mode);

// tail指向同步队列的尾节点
Node pred = tail;
// 如果tail不为空,则进行一个快速插入(将节点添加到队列尾部)
if (pred != null) {
	node.prev = pred;
	if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
	}
}
// 否则使用enq进行可能包含初始化的入队操作(尝试循环不断的添加)
enq(node);
return node;
}

addWaiter()方法会首先会判断,能否成功将节点添加到队列尾部,如果添加失败,在调用enq方法(使用循环不断重试)进行添加

下面是enq()方法的源码

/**
* Inserts node into queue, initializing if necessary. See picture
* above.<br>
* 将节点插入队列,如有必要,进行初始化。
* 
* @param node
*            the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
	for (;;) {

	Node t = tail;

	// 同步队列采用的懒初始化的方式
	// 初始化时head和tail都会被设置为null,
	// 当第一次访问时才会创建head对象,并把尾指针指向head。
	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()方法仅仅是将节点加到了同步队列的尾部,并没有阻塞线程,线程阻塞的操作是在acquireQueued()中完成的

acquireQueued方法源码:

/**
* Acquires in exclusive uninterruptible mode for thread already in queue.
* Used by condition wait methods as well as acquire.<br>
* 以排队的不间断模式获取线程。 使用条件等待方法以及获取。
*
* @param node
*            the node
* @param arg
*            the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
	boolean interrupted = false;
	for (;;) {
		final Node p = node.predecessor();
		// 如果当前节点的前驱节点是head,就使用自旋(循环)的方式不断请求锁。
		if (p == head && tryAcquire(arg)) {
				// 成功获得锁,将当前节点置为head节点,同时删除head节点
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
		}

	// shouldParkAfterFailedAcquire方法检查是否可以挂起线程
	// 如果可以挂起线程,会调用parkAndCheckInterrupt()方法挂起线程
	// 如果parkAndCheckInterrupt返回true,表明当前线程是因为中断而退出挂起状态的,
	// 所以要将interrupted设为true,表明当前线程被中断过。
	if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
	interrupted = true;
	}
	} finally {
	if (failed)
		cancelAcquire(node);
	}
}


下面是shouldParkAfterFailedAcquire方法的源码:

/**
* Checks and updates status for a node that failed to acquire. Returns true
* if thread should block. This is the main signal control in all acquire
* loops. Requires that pred == node.prev
*
* @param pred
*            node's predecessor holding status
* @param node
*            the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 当前节点的前驱节点的等待状态
	int ws = pred.waitStatus;
	// 如果前驱节点的等待状态为SIGNAL,我们就可以将当前节点对应的线程挂起
	if (ws == Node.SIGNAL)
			/*
			 * This node has already set status asking a release to signal it,
			 * so it can safely park.
			 */
			return true;
	if (ws > 0) {
			/*
			 * Predecessor was cancelled. Skip over predecessors and indicate
			 * retry.
			 */
			// ws大于0,表明当前线程的前驱节点处于CANCELED的状态,
			// 所以我们需要从当前节点开始往前查找,直到找到第一个不为CAECELED状态的节点
			do {
					node.prev = pred = pred.prev;
			} while (pred.waitStatus > 0);
			pred.next = node;
	} else {
			/*
			 * waitStatus must be 0 or PROPAGATE. Indicate that we need a
			 * signal, but don't park yet. Caller will need to retry to make
			 * sure it cannot acquire before parking.
			 */
			compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}


parkAndCheckInterrupt 方法对线程进行阻塞:

/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
	// 挂起当前线程
	LockSupport.park(this);
	// 可以通过调用 interrupt 方法使线程退出 park 状态,
	// 为了使线程在后面的循环中还可以响应中断,会重置线程的中断状态。
	// 这里使用 interrupted 会先返回线程当前的中断状态,然后将中断状态重置为 false,
	// 线程的中断状态会返回给上层调用函数,在线程获得锁后,
	// 如果发现线程曾被中断过,会将中断状态重新设为 true
	return Thread.interrupted();
}



3. 独占锁的释放

释放独占锁的流程:



通过release()方法,我们可以释放互斥锁,下面是release()源码:

/**
* Releases in exclusive mode. Implemented by unblocking one or more threads
* if {@link #tryRelease} returns true. This method can be used to implement
* method {@link Lock#unlock}.
*
* @param arg
*            the release argument. This value is conveyed to
*            {@link #tryRelease} but is otherwise uninterpreted and can
*            represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
	if (tryRelease(arg)) {
			Node h = head;
			// waitstatus为0,证明是初始化的空队列或者后继节点已经被唤醒了
			if (h != null && h.waitStatus != 0)
					unparkSuccessor(h);
			return true;
	}
	return false;
}


在独占模式下释放锁时,是没有其他线程的竞争的,所以处理起来会简单些。

首先尝试释放锁,如果失败就直接返回(失败不是因为多线程竞争,而是线程本身就不用有锁)

如果成功的话,会检查h的状态,然后调用unparkSuccessor()方法

/**
* Wakes up node's successor, if one exists.<br>
* 唤醒节点的后继,如果存在。
*
* @param node
*            the node
*/
private void unparkSuccessor(Node node) {
	/*
	 * If status is negative (i.e., possibly needing signal) try to clear in
	 * anticipation of signalling. It is OK if this fails or if status is
	 * changed by waiting thread.
	 */
	int ws = node.waitStatus;

	// 将head节点的状态置为0,表明当前节点的后继节点已经被唤醒了
	// 不再需要再次唤醒,修改ws状态主要作用于release的判断
	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.
	 */
	Node s = node.next;
	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)
			LockSupport.unpark(s.thread);
}

在 unparkSuccessor 方法中,如果发现头节点的后继结点为 null 或者处于 CANCELED 状态,

会从尾部往前找(在节点存在的前提下,这样一定能找到)离头节点最近的需要唤醒的节点,然后唤醒该节点。



4. 共享锁获取与释放

共享式获取 与 独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。

我们先简单了解下独占模式共享模式


独占模式:保证一次只有一个线程可以经过阻塞点,获取锁(许可)

共享模式:可以允许多个线程经过阻塞点,获取同一个锁(许可)


以文件的查看为例;

共享模式:

如果一个程序在对其进行读取操作,同时,对这个文件的写操作就被阻塞,同时另一个程序对其进行同样的读操作是可以进行的。

独占模式:

如果一个程序在对其进行写操作,那么其他所有的读与写操作在这一时刻就被阻塞,直到这个程序完成写操作。


感觉这个图比原书中更能体现共享与独占的关系(感谢图作者)




共享锁的获取

共享锁获取的流程



通过调用同步器的acquireShared(int arg)方法可以共享式的获取同步锁;

源码如下:

/**
* Acquires in shared mode, ignoring interrupts. Implemented by first
* invoking at least once {@link #tryAcquireShared}, returning on success.
* Otherwise the thread is queued, possibly repeatedly blocking and
* unblocking, invoking {@link #tryAcquireShared} until success.<br>
* <p>
* 以共享模式获取,忽略中断。 通过首次调用{@link #tryAcquireShared}来实现,成功返回。
* 否则线程排队,可能会重复阻塞和解除阻塞,调用{@link #tryAcquireShared}直到成功。
*
* @param arg
*            the acquire argument. This value is conveyed to
*            {@link #tryAcquireShared} but is otherwise uninterpreted and
*            can represent anything you like.
*/
public final void acquireShared(int arg) {

	// 调用tryAcquireShared()方法尝试获取同步状态
	// 返回值大于等于0时,表示能够获取到同步状态
	// 如果返回结果小于0,证明没有获取到共享资源
	if (tryAcquireShared(arg) < 0)
			doAcquireShared(arg);
}

/**
* Acquires in shared uninterruptible mode.<br>
* 以共享不间断模式获取。
* 
* @param arg
*            the acquire argument
*/
private void doAcquireShared(int arg) {
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true;
	try {
			boolean interrupted = false;
			for (;;) {
					final Node p = node.predecessor();
					// 如果当前节点的前驱节点为头结点时,尝试获取同步状态
					if (p == head) {
							int r = tryAcquireShared(arg);

	// 如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出
	if (r >= 0) {
			setHeadAndPropagate(node, r);
			p.next = null; // help GC
			if (interrupted)
					selfInterrupt();
			failed = false;
			return;
	}
	}
	if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
			interrupted = true;
	}
	} finally {
			if (failed)
					cancelAcquire(node);
	}
}


当一个节点获取到共享节点之后,它除了将自身设置为head节点之外,还会判断一下是否满足唤醒后继节点的条件,

如果满足,就唤醒后继节点,后继节点获取到锁之后,会重复这个过程,直到判断条件不成立。

就类似于:考试时,从第一排往最后传试卷,第一排留下一份,然后将剩余的传给后一排,后一排会重复这个过程。

如果传到某一排卷子没有了,那么位于这排的人就要等待了,直到老师又给了他新的卷子。



共享锁的释放

共享锁的释放流程



通过releaseShared()方法会释放共享锁,

源码如下:

/**
* Releases in shared mode. Implemented by unblocking one or more threads if
* {@link #tryReleaseShared} returns true.<br>
* 以共享模式发布。 如果{@link #tryReleaseShared}返回true,则通过解除阻塞一个或多个线程来实现。
*
* @param arg
*            the release argument. This value is conveyed to
*            {@link #tryReleaseShared} but is otherwise uninterpreted and
*            can represent anything you like.
* @return the value returned from {@link #tryReleaseShared}
*/
public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
			doReleaseShared();
			return true;
	}
	return false;
}
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。


后面的内容再说...妈呀,看的蛋疼...


参考资料:

1. http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-overview.html

2. https://my.oschina.net/andylucc/blog/651982

3. https://zhuanlan.zhihu.com/p/24607631

4. http://luojinping.com/2015/06/19/AbstractQueuedSynchronizer%E8%AF%A6%E8%A7%A3/#独占模式

5. https://coderbee.net/index.php/concurrent/20131209/614

6. http://blog.zhangjikai.com/2017/04/15/%E3%80%90Java-%E5%B9%B6%E5%8F%91%E3%80%91%E8%AF%A6%E8%A7%A3-AbstractQueuedSynchronizer/

7. http://www.cnblogs.com/xrq730/p/4979021.html

8. http://www.cnblogs.com/xrq730/p/7056614.html <再谈AbstractQueuedSynchronizer1:独占模式>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值