深入理解AQS-Java多线程中锁的使用之学习笔记

144 阅读6分钟

前言

关于多线程的知识点总结了一个图谱,分享给大家:

多线程并发编程.jpg

AQS设计初衷

比如有这么个场景:有四个线程由于业务需求需要同时占用某资源,但该资源在同一个时刻只能被其中唯一线程所独占。那么问题来了,怎么标识资源已经被占用?若支持可重入,怎么标识冲入次数?对于争夺资源的线程怎么设计,是公平锁还是非公平锁?怎么判断上一个线程是否已经释放资源?

image.png

这就是AQS的设计初衷了。AQS 是一个集同步状态管理、线程阻塞、线程释放及队列管理功能与一身的同步框架。其核心思想是当多个线程竞争资源时会将未成功竞争到资源的线程构造为 Node 节点放置到一个双向 FIFO 队列中。被放入到该队列中的线程会保持阻塞直至被前驱节点唤醒。

资源占用标识,重入

AbstractQueuedSynchronizer中有这么个变量

/** * The synchronization state. */
private volatile int state;

以及下面一堆方法

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
	return state;
}
/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
	state = newState;
}
/**
 * Atomically sets synchronization state to the given updated
 * value if the current state value equals the expected value.
 * This operation has memory semantics of a {@code volatile} read
 * and write.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that the actual
 *         value was not equal to the expected value.
 */
protected final Boolean compareAndSetState(int expect, int update) {
	return U.compareAndSwapint(this, STATE, expect, update);
}

在AQS中由一个int类型的state变量管理同步状态

1.state=0 资源未被线程使用

2.state=1 资源被线程使用

3.state=N (N>1) 资源被某线程重入N次

凭借state,我们可以很轻松标识资源的状态以及线程的重入次数

设计模式

在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:

1.锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;

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

锁和同步器很好地隔离了使用者和实现者所需关注的领域。实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

模板方法模式参考文章: 模板方法模式

多线程处理

回顾下上面的问题:

1.线程按照什么顺序拿取资源?

2.资源使用结束后其他线程如何得知?

AQS的基本思想—CLH队列锁

CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋

PS:八卦一下为啥叫CLH队列锁,CLH队列锁即Craig, Landin, and Hagersten (CLH) lock,这玩意之所以不好记是因为CLH是三个人名字的首字母!!!

其结构如下:

image.png

解释下结构:

1.节点QNode,由指针、线程、locked三部分组成

1.1指针指向前一个QNode的myPre

1.2locked=true 标识需要获取资源且不释放资源,为false则表示释放了锁

2.由链表构成的队列,先进先出。每个新来的线程或者已经释放资源的线程都被包装成QNode插到队尾(公平锁的方式),指针指向前驱节点的myPre

当一个线程要获取锁时

1.创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用

2.线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred;线程B需要获得锁,同样的流程再来一遍

3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)

4.当一个线程需要释放锁时,将当前结点的locked域设置为false同时回收前驱结点。前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁

AQS的优化

AQS的基本思想是CLH,在具体实现上做了优化

1.0AQS不是单链表,而是双向链表

2.自旋不会自旋很多次,一般自旋两三次,两次失败当前线程进入阻塞

一个可重入锁的栗子

/**
 * Created by Vola on 2021/4/5.
 */
public class SelfZLock implements Lock {
	//  自定义 同步器
	private static class Sync extends AbstractQueuedSynchronizer{
		/**
         * 是否是独占状态
         * @return
         */
		@Override
		        protected Boolean isHeldExclusively() {
			return getState() > 0;
		}
		/**
         *      获得锁
         * @param arg
         * @return
         */
		@Override
		        protected Boolean tryAcquire(int arg) {
			if (compareAndSetState(0, 1)) {
				setExclusiveOwnerThread(Thread.currentThread());
				return true;
			} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
				//  如果是当前线程,可重入
				setState(getState() + 1);
				return true;
			}
			return false;
		}
		/**
         *      释放锁,考虑重入情况的释放
         * @param arg
         * @return
         */
		@Override
		        protected Boolean tryRelease(int arg) {
			if (getExclusiveOwnerThread() != Thread.currentThread()) {
				throw new IllegalMonitorStateException();
			}
			if (getState() == 0) {
				throw new IllegalMonitorStateException();
			}
			setState(getState() - 1);
			if (getState() == 0) {
				setExclusiveOwnerThread(null);
			}
			return true;
		}
		/* 返回一个Condition,每个condition都包含了一个condition队列*/
		Condition newCondition() {
			return new ConditionObject();
		}
	}
	private final Sync sync = new Sync();
	@Override
	    public void lock() {
		System.out.println(Thread.currentThread().getName()+" ready get lock");
		sync.acquire(1);
		System.out.println(Thread.currentThread().getName()+" already got lock");
	}
	@Override
	    public Boolean tryLock() {
		return sync.tryAcquire(1);
	}
	@Override
	    public void unlock() {
		System.out.println(Thread.currentThread().getName()+" ready release lock");
		sync.release(1);
		System.out.println(Thread.currentThread().getName()+" already released lock");
	}
	@Override
	    public Condition newCondition() {
		return sync.newCondition();
	}
	public Boolean isLocked(){
		return sync.isHeldExclusively();
	}
	public Boolean hasQueuedThreads() {
		return sync.hasQueuedThreads();
	}
	public void lockInterruptibly() throws InterruptedException {
		sync.acquireInterruptibly(1);
	}
	public Boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
		return sync.tryAcquireNanos(1, unit.toNanos(timeout));
	}
}

最后

我这边整理了一份Java多线程资料文档、Spring系列全家桶、Java的系统化资料:(包括Java核心知识点、面试专题和20年最新的互联网真题、电子书等)有需要的朋友可以关注公众号【程序媛小琬】即可获取。