更多内容请看:Java并发编程学习笔记
AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式同步器。这个类java.util.concurrent.locks包,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。 所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
换句话说AQS基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、栅栏都是AQS的衍生物。
如图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:
getState();setState();compareAndSetState();
从使用层面来说,AQS 的功能分为两种:独占和共享
当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化。首先看一下添加线程涉及到两个:
head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,
如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。
自定义同步器实现的时候主要实现下面几种方法:
举例说明独占和共享的具体实现
ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1。之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。
CountDownLatch为例:任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
CAS的全称是CompareAndSwap,比较并交换,是Java保证原子性的一种重要方法,也是一种乐观锁的实现方式。
它需要先提前一步获取旧值,然后进入此方法比较当下的值是否与旧值相同,如果相同,则更新数据,否则退出方法,重复一遍刚才的动作。由此可见,CAS方法是非堵塞的。CAS方法需要三个参数,变量内存值、旧的预期值、数据更新值。
CAS的伪代码可以表示为:
do{
获取备份旧数据;
准备更新的数据;
}while( !CAS( 内存地址,备份的旧数据,新数据 ))
CAS的源码
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false。
具体分析上面的代码:
Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、
Hadoop、Kafka 等;
一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存
里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节
偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置。所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量。
在 unsafe.cpp 文件中,可以找到 compareAndSwarpInt 的实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject
unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //将 Java 对象解析成 JVM 的 oop(普通对象指针),
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比较并替换, x 示需要更新的值,addr 表示 state 在内存中的地址,e 表示预期值UNSAFE_END
之后会整理ReentrantLock、Condition、CountDownLatch、Semaphore、CyclicBarrier等常用的并发工具的原理及使用。
部分转自:https://blog.csdn.net/mulinsen77/article/details/84583716