AQS(一)
Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器
实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获
取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定
义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
AQS具备的特性:阻塞等待队列,共享/独占, 公平/非公平,可重入,允许中断
例如Java.concurrent.util当中同步器的实现如Lock,Latch,Barrier等,都是基于AQS框架实现:
一般通过定义内部类Sync继承AQS,
将同步器所有调用都映射到Sync对应的方法.
AQS内部维护属性volatile int state (32位)
state:表示资源的可用状态
State三种访问方式:
getState()、setState()、compareAndSetState() .
AQS定义两种资源共享方式 :
Exclusive-独占,只有一个线程能执行,如ReentrantLock .
Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch.
AQS定义两种队列
同步等待队列 ,条件等待队列
-
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只
需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护
(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器 实现时主要实现以下几种方法:isHeldExclusively():该线程是否正在独占资源。只有用到 condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败 则返回false。 tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败 则返回false。 tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败; 0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许 唤醒后续等待结点返回true,否则返回false。
同步队列
ReentrantLock加锁和释放锁的底层原理
好了,那么现在如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情呢?
很简单,这个AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。初始状态下 ,这个state的值是0。
另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
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;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
接着线程1跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。
如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。
一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。所以大家看下面的图,就是线程1跑过来加锁的一个过程。
这个ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。
可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。
其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。
接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?
我们来看看锁的互斥是如何实现的?线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!
接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了
所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
接下来,会从等待队列的队头唤醒线程2重新尝试加锁。
好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。
此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。
最后再来一张图,大家来看看这个过程。
问题又来了 ,这些是如何进入阻塞队列呢?
首先看下刚开始·创建节点的时候
/**
* 节点加入CLH同步队列
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//队列为空需要初始化,创建空的头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//set尾部节点
if (compareAndSetTail(t, node)) {//当前节点置为尾部
t.next = node; //前驱节点的next指针指向当前节点
return t;
}
}
}
}
=========================================================================
来深入几个点:1.线程是如何被阻塞的
/**
* 阻塞当前节点,返回当前Thread的中断状态
* LockSupport.park 底层实现逻辑调用系统内核功能 pthread_mutex_lock 阻塞线程
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞
return Thread.interrupted();
}
本质上Unsafe魔术类中的park()和unpark()去进行线程阻塞和唤醒的
本文地址:https://blog.csdn.net/qq_40669764/article/details/107284276