Semaphore ,aqs,ReentrantLock ,Condition,CopyOnWriteArrayList 源码解读
一、Semaphore
Semaphore
通过设置一个固定数值的信号量,并发时线程通过 acquire()
获取一个信号量,如果能成功获得则可以继续执行,否则将阻塞等待,当某个线程使用 release()
释放一个信号量时,被阻塞的线程则可以被唤醒重新争抢信号量。根据该特征可以有效控制线程的并发数。
那 Semaphore
是如何控制并发的呢,本篇文章带领大家一起解读下 Semaphore
的源码。
在进行源码分析前,先回顾下 Semaphore
是如何使用的,例如下面一个案例:
public class Test {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("线程:" + Thread.currentThread().getName() + " 执行, 当前时间:" + LocalDateTime.now().toString());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
运行之后,可以看到下面日志:
可以看到每次都是 3
个并发。
在本专栏前面讲解 AQS
源码的时候提到 Semaphore
是基于 AQS
实现的,那是如何使用的 AQS
呢?
在 AQS
中,如果需要使用AQS
的特征则需要子类根据使用的场景,重写下面方法:
//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)
由于这里 Semaphore
的特性,所以下面我们只需关注共享模式下的几个方法即可。
说明:由于 Semaphore
的实现依赖于 AQS
,因此需要对 AQS
有一定的了解,不了解的小伙伴可以看下这篇对 AQS
源码分析的文章,和当前文章在同一专栏:
二、Semaphore 中 Sync、FairSync、NonfairSync
2.1 Sync、FairSync、NonfairSync
在声明 Semaphore
时,有两种方式,一种是使用只有一个 permits
参数的构造函数,一种则需要多增加一个 fair
参数:
new Semaphore(3);
new Semaphore(3, true);
当使用只有一个 permits
参数的构造函数声明时,则是创建了一个 NonfairSync
对象:
通过需要多增加一个 fair
参数的构造函数时,则可以根据传入的 fair
选择创建一个 FairSync
对象:
这里也不难理解 NonfairSync
和 FairSync
其实可以理解为 Semaphore
中的非公平锁和公平锁两种类型。
点到这两个类中,可以看到都继承自 Sync
类:
而 Sync
类,则继承自 AQS
:
到这里,我们就可以寻找几个关键的方法,在AQS
中共享模式下,两大关键的方法是交由子类进行实现的,分别是 tryAcquireShared
尝试获取资源,和 tryReleaseShared
尝试释放资源。
首先来看 tryAcquireShared
尝试获取资源:
通过 Sync
类的实现源码发现并没有重写 tryAcquireShared
方法,那该方法肯定在下面的FairSync
和 NonfairSync
子类中,分别看下源码确实存在重写的方法:
2.2 NonfairSync 下的 tryAcquireShared
这里先分析下 NonfairSync
的 tryAcquireShared
实现逻辑,可以看到又调用了 nonfairTryAcquireShared
就是 Sync
类中的 nonfairTryAcquireShared
,从命名上可以分析出就是非公平锁的尝试获取资源的操作,直观就是非公平锁下获取锁的操作:
进入到 Sync
类中的 nonfairTryAcquireShared
方法中,可以明显看到一个自旋的操作,在循环中首先获取到 AQS
中的共享资源 state
,并对其进行 - acquires
(默认为 1
,后面会进行说明)操作,其实就是 -1
操作,如果减去的值小于 0
或者修改 state
成功,就返回当前减去的值,否则就自旋的方式再次重试:
上一步的操作主要做了什么目的呢,其实从 Sync
的构造方法就可以看出,创建 Semaphore
传递的 permits
参数被赋值给了 AQS
中的 state
,那此时 state
就记录的当前剩余信号量的大小,获取资源就要进行 -1
标识消耗了一个,最后将减去的值返回出去表示剩余的资源,如果信号量小于 0
了,则表示获取资源失败,直观就是获取锁失败。因为在 AQS
中对 tryAcquireShared
方法的判断是小于 0
时,进行线程的入列和挂起等待。
2.3 FairSync下的 tryAcquireShared
在 FairSync
类下的 tryAcquireShared
方法中,和前面 NonfairSync
类似,但不同的是,会首先进行 hasQueuedPredecessors
方法的判断:
下面进到 hasQueuedPredecessors
的方法中,可以看到是由 AQS
提供的方法,主要就是判断当前节点线程的前面是否还有等待的线程,因为 FairSync
实现的是公平锁的原则,如果当前线程前面还有等待线程,则获取锁资源也轮不到自个,让前面的老大先来,所以直接返回 -1
表示获取资源失败:
2.4 tryReleaseShared
到这里已经了解到了 tryAcquireShared
尝试获取资源的逻辑,上面提到了两个重要方法,还有一个 tryReleaseShared
没有分析,还是首先看 Sync
类中是否有重写该方法:
通过源码可以看到,在 Sync
类中就已经对 tryReleaseShared
进行了重写,而 NonfairSync
和 FairSync
中都没有重写该方法,那释放资源就是走的 Sync
类下的 tryReleaseShared
方法:
在该方法同样使用了自旋,首先获取到 AQS
中的共享资源 state
,然后进行 + releases
(默认情况下为 1
,后面会说明),其实就是进行 +1
操作,并使用新的值修改 state
,如果修改失败的话则在自旋中继续修改,直到成功后返回 true
,表示释放资源成功。
看到这里就会发现获取资源和释放资源,无非就是对 AQS
中的共享资源 state
进行操作。理解了这两大核心的方法后,下面就可以看如何运用在 Semaphore
中的了。
三、semaphore.acquire()
通过 semaphore.acquire()
可以获取一个信号量,如果获取不到则阻塞等待,那semaphore.acquire()
主要做了什么呢?
下面点到该方法中,可以看到又调用了 sync.acquireSharedInterruptibly
方法,其实就是 AQS
中的 acquireSharedInterruptibly
方法,注意这里传递的参数为 1
,对应上面括号中的说明:
在 AQS
的 acquireSharedInterruptibly
方法中,首先会使用子类的 tryAcquireShared
方法获取资源,如果资源数小于 0
,则认为获取失败,下面使用 doAcquireSharedInterruptibly
进行加入队列并挂起阻塞:
关于AQS
如何加入队列和挂起,可以参考文章开始的链接中对 AQS
源码的解读。
四、semaphore.release()
上面在获取不到可用的资源时,则会被 AQS
挂起,因此这里还需要进行释放资源。
下面点到 semaphore.release()
方法中,可以看到又调用了 sync.releaseShared
,其实就是 AQS
中的 releaseShared
方法,注意这里参数默认为 1
,对应上面括号中的说明:
在 AQS
的 releaseShared
方法中,会首先调用子类的 tryReleaseShared
释放资源,释放成功后,会使用 doReleaseShared
进行挂起线程的唤醒:
关于releaseShared
方法的源码解读可以参考文章开始的链接中对 AQS
源码的解读。
三、总结
通过阅读 Semaphore
的源码可以发现,大量依赖于 AQS
中提供的方法,如果有阅读过本专栏对 ReentrantLock
锁源码的分析,可以发现相似度极高,都是使用 AQS
所提供的的特征实现某些场景的应用。
AQS 源码解读
一、AQS
AQS
是 AbstractQueuedSynchronizer
的简称,又称为同步阻塞队列,是 Java
中的一个抽象类。在其内部维护了一个由双向链表实现的 FIFO
线程等待队列,同时又提供和维护了一个共享资源 state
,像我们平常使用的 ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask
等都是基于AQS
进行实现的。
AQS
可以实现什么功能呢?在 AQS
中不考虑资源的获取和释放,主要关注资源获取不到时,如何将线程加入队列以及阻塞,当释放资源后,如何再进行线程的出列和唤醒。而对于资源的操作则交予具体实现的子类进行完成。
在此基础上 AQS
为了使线程的控制可以更灵活,又提供了两种同步模型,独占模式和共享模式:
-
独占模式:表示并发情况下只有一个线程能执行,其余则需等待,例如
Lock
锁,一次只能有一个线程获取到锁。 -
共享模式:允许多线程根据规则执行,例如
Semaphore
进行多个线程的协调。
AQS
已经帮我们实现了队列的维护,以及线程的等待和唤醒,但是具体资源的获取和释放都需要由继承类实现,对于资源的获取和释放也是区分了独占模式和共享模式,相应方法如下:
//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)
例如在 ReentrantLock
公平锁中,tryAcquire
的实现逻辑如下:
protected final boolean tryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// AQS 中共享 state
int c = getState();
if (c == 0) {
// 如果队列中没有其他线程,并对state进行修改,
// 如果修改成功则设置独占锁的线程为当前线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果独占线程就是当前线程,则是重入的场景,对 state + 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果都没成功,则获取锁失败
return false;
}
}
可以看到在 ReentrantLock
公平锁中,通过 state
的值来标识是否有锁资源可用,并且重入情况下也是对 state
的值进行修改标识 ,对于 state
的修改和判断是否有等待队列线程, AQS
中都提供了相应的方法。
在 AQS
中几个核心的方法如下,同样区分了独占模式和共享模式:
// 返回共享资源的当前值
final int getState()
// 设置共享资源的值
final void setState(int newState)
// CAS设置共享资源的值
final boolean compareAndSetState(int expect, int update)
// 独占模式获取同步资源,会调用重写的tryAcquire(int arg),
// 如果获取成功,则不做任何处理,否则将会加入同步队列并挂起线程等待
final void acquire(int arg)
// 独占模式式获取同步资源,但是可以响应中断
final void acquireInterruptibly(int arg)
// 独占模式获取同步资源,但多出了超时时间,
// 如果当前线程在 nanosTimeout 时间内没有获取到同步资源,
// 那么将会返回false,否则返回true
final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 独占模式式释放同步资源,会调用重写的 tryRelease(int arg) 方法,
// 在释放同步资源之后,会将同步队列中第一个节点包含的线程唤醒
final boolean release(int arg)
// 共享模式式获取同步资源,会调用重写的 tryAcquireShared(int arg) ,
// 如果当前线程未获取到同步资源,会加入同步队列等待,
// 和独占式的区别这里 tryAcquireShared(int arg) < 0 时才认为未获取到资源
final void acquireShared(int arg)
// 共享模式式获取同步资源,可以响应中断
final void acquireSharedInterruptibly(int arg)
// 共享模式获取同步资源,但多出了超时时间
final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 共享式释放同步资源,会调用重写的 tryReleaseShared(int arg) 方法,
// 在释放同步资源之后,会将同步队列中第一个节点包含的线程唤醒
final boolean releaseShared(int arg)
下面一起从源码的角度,分析 AQS
是如何实现线程的协调和管理的。
二、共享资源 state
共享资源 state
就是 AQS
中的一个 int
类型的全局变量,使用了 volatile
进行修饰,保证了多线程下的数据可见性,并且 AQS
为其提供了普通和 CAS
方式的修改方法,该共享资源主要用来做资源的标记。
例如:
在 ReentrantLock
锁中用来表示是否获取到锁,默认情况 0
表示无锁状态,获取到锁后进行 +1
,如果是重入的场景下同样进行 +1
,最后释放锁后再进行 -1
。
在 Semaphore
中用来表示信号量的标记,当获取信号量时 state
进行 -1
,释放信号量再进行 +1
:
三、FIFO 阻塞队列
在 AQS
中阻塞队列采用双向链表进行实现,具体源码如下:
//等待队列节点类,双向链表
static final class Node {
// 标记,指示节点正在共享模式下等待
static final Node SHARED = new Node();
// 标记,指示节点正在独占模式下等待
static final Node EXCLUSIVE = null;
// waitStatus值表示线程已取消
static final int CANCELLED = 1;
// waitStatus值表示后继线程需要唤醒
static final int SIGNAL = -1;
// waitStatus值,表示线程正在等待状态
static final int CONDITION = -2;
// waitStatus值指示下一个被获取的应该无条件的传播
static final int PROPAGATE = -3;
// 线程等待状态
volatile int waitStatus;
// 上一个节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 当前线程
volatile Thread thread;
// 节点的模式,独占还是贡献
Node nextWaiter;
// 是否为共享模型
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;
}
}
其中通过 nextWaiter
表示当前线程是独占模型还是共享模式,线程的所属状态使用 waitStatus
来进行表示,其中包括:
- 默认值为
0
,表示当前节点在sync
队列中,等待着获取资源。 CANCELLED
,值为1
,表示当前的线程被取消。SIGNAL
,值为-1
,释放资源后需唤醒后继节点。CONDITION
,值为-2
, 等待condition
唤醒。PROPAGATE
,值为-3
,工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点。
四、独占模式 acquire 获取资源
在 AQS
acquire()
方法中,首先调用子类的 tryAcquire
获取资源,如果资源获取成功则不做任何处理,如果失败则首先使用 addWaiter
将当前线程加入到队列中,并指定 Node
的类型为独占模式:
在 addWaiter
方法中,会将当前线程的 Node
加入到队列的尾端,如果尾节点为空或修改尾节点失败则进入到 enq
中使用自旋的方式修改:
在 enq
方法中可以看出,当为节点为空时,也就是队列中无数据时,会初始化一个空的 Head
节点。
再回到 acquire()
方法,加入队列后会进入到 acquireQueued
方法中,在该方法循环中如果当前节点 的pred
上一个节点是 head
节点的话,那该节点不就是第一个节点吗,因为从上面就可以看出,初始情况下 head
是一个空的 node
,那 head
的下一个节点不就是第一个进入到队列的节点了,这种情况下遵循队列先进先出的原则,再次尝试是否能获取到资源,如果可以成功获取资源到则将当前节点置为 head
节点,同时再次将 head
节点置为空 node
,此时线程也无需阻塞可以直接执行:
但是如果当前节点的上一个节点不是 head
节点,或者没有获取到资源,则此时需要进行挂起阻塞,下面首先会触发 shouldParkAfterFailedAcquire
方法,这里先看后面的 parkAndCheckInterrupt
方法,该方法主要做了将线程挂起阻塞的作用,采用 LockSupport.park
进行线程的阻塞:
再来看 shouldParkAfterFailedAcquire
方法就是控制当前线程是否需要挂起,这里就需要使用到 Node
中的 waitStatus
,在该方法中有三种类型的判断:
- 如果当前是
SIGNAL
状态则可以直接挂起 - 当
waitStatus
大于0
时,在Node
中waitStatus
大于0
的状态就是CANCELLED
状态,也就是标识线程被取消了,此时这种线程进行阻塞也就没有意义了,那就一直循环向上取线程未被取消的作为当前节点,继续执行。 - 当
waitStatus
小于等于0
时,将状态置为SIGNAL
类型
后面当阻塞的线程被唤醒后,会继续在 acquireQueued
的循环中,不断找寻第一个入队的线程进行尝试获取资源操作。
五、独占模式 release 释放资源
在 release
方法中,首先会调用子类的 tryRelease
方法释放资源:
然后会将当前的 head
节点传入 unparkSuccessor
方法中,在该方法中首先将该Node
节点的 waitStatus
修改到默认的 0
值,然后获取到下一个节点,因为 head
节点始终保持为空节点,下一个节点才是真正的队列中第一个线程。但如果下一个节点为空的话,或者已经被取消了,则循环从 tail
节点向上找最前面正常的节点,最后直接使用 LockSupport.unpark
唤醒该节点的线程:
六、共享模式 acquireShared 获取资源
在 acquireShared
方法中,会首先调用子类的 tryAcquireShared
方法获取资源,但与独占模式不同的是,这里当资源的数量小于 0
时,则认为获取资源失败:
当资源获取失败时,会进入到 doAcquireShared
方法,在该方法中同样先将自己加入到阻塞队列中,将 Node
的类型设为 Node.SHARED
共享模式:
下面的判断逻辑和独占模式差不多,取当前节点的上一个节点,如果是 head
节点,那当前节点便是队列的第一个线程,此时则可以尝试获取资源,如果资源大于 0
认为获取资源成功,则将当前节点置为 head
节点:
在 setHeadAndPropagate
方法中,与独占模式不同,将当前节点置为 head
节点后并没有进行置空操作,而且又会判断资源大于 0
的话,通过 doReleaseShared
唤醒更多的线程继续执行:
这里 doReleaseShared
方法的逻辑,在下面 releaseShared
解读时进行解释:
回到 doAcquireShared
方法中,下面 shouldParkAfterFailedAcquire
和 parkAndCheckInterrupt
则和独占模式调用方法相同,将符合条件的线程进行阻塞:
后面当阻塞的线程被唤醒后,会继续在 doAcquireShared
的循环中,不断找寻第一个入队的线程进行尝试获取资源操作。
七、共享模式 releaseShared 释放资源
在 releaseShared
方法中,会首先调用子类的 tryReleaseShared
方法释放资源:
释放资源后会进到 doReleaseShared
方法唤醒等待的线程,对 head
节点进行唤醒:
当 head
节点唤醒后,会和 doAcquireShared
的方法中的 setHeadAndPropagate
形成呼应,如果获取到的资源数大于 0
则继续使用 doReleaseShared
进行唤醒,从而控制多个线程执行。
八、总结
AQS
没有限制具体某个场景的应用,但通过其内部维护的 FIFO
队列和共享资源 state
便可以实现很多种不同的场景,在阅读了 AQS
源码后,应该有了更深入的理解,后面再去看 ReentrantLock、Semaphore
等的源码会发现很容易理解。
ReentrantLock 源码解读
一、ReentrantLock
ReentrantLock
是 java
JUC
中的一个可重入锁,在上篇文章讲解 AQS
源码的时候提到 ReentrantLock
锁是基于 AQS
实现的,那是如何使用的 AQS
呢,本篇文章一起带大家看下 ReentrantLock
的源码。
在 AQS
中,如果需要使用AQS
的特征则需要子类根据使用的场景,重写下面方法,
//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)
由于这里 ReentrantLock
锁的特性,所以下面我们只需关注独占模式下的几个方法即可。
说明:本文中关于 AQS
中的方法没有做过多的解释,不了解的小伙伴可以看下这篇对 AQS
源码分析的文章,和当前文章在同一专栏:
下面一起开始 ReentrantLock
源码的分析:
二、ReentrantLock 的 Sync、FairSync、NonfairSync
2.1 Sync、FairSync、NonfairSync
在声明 ReentrantLock
锁时,有两种方式,一种是无参构造函数,一种则需要指定一个 fair
参数:
new ReentrantLock();
new ReentrantLock(false);
当使用无参构造函数声明时,则是创建了一个 NonfairSync
对象:
通过有参的构造函数,则根据传入的 fair
可以选择创建一个 FairSync
对象:
其实这里也不难理解 NonfairSync
和 FairSync
其实就是ReentrantLock
锁中的非公平锁和公平锁两种类型。
点到这两个类中,可以看到都继承自 Sync
类:
而 Sync
类,则继承了 AQS
:
到这里了,我们寻找几个关键的方法,在AQS
中独占模式下,两大关键的方法是交由子类进行实现的,分别是 tryAcquire
尝试获取资源,和 tryRelease
尝试释放资源。
首先来看 tryAcquire
尝试获取资源:
通过 Sync
类的实现源码发现并没有重写 tryAcquire
方法,那该方法肯定在下面的子类FairSync
和 NonfairSync
,分别看下源码确实存在重写的方法:
2.2 NonfairSync 下的 tryAcquire
首先看下 NonfairSync
的 tryAcquire
实现逻辑,可以看到又调用了 nonfairTryAcquire
就是 Sync
类中的 nonfairTryAcquire
,从命名上可以分析出就是非公平锁的尝试获取资源,直观就是非公平锁下获取锁操作:
进入到 Sync
类中的 nonfairTryAcquire
中,可以看到首先获取到 AQS
中的共享资源 state
,如果 state
等于 0
,则将 state
的值修改为 acquires
(默认为1
,下面会分析到),并设置AQS
的独占线程为当前线程,并返回 true
,说白了不就是获取到锁了吗,那就可以理解为 state
等于 0
即是无锁的状态,下面将 state
的值修改为 acquires
就是获取到锁了,改变资源的状态:
接着如果 state
的值不是 0
,则当前锁已经被别的线程持有了,这里又判断了下,如果持有锁的线程正好是当前的线程,那不就是锁的重入吗,这种情况下可以直接获得锁,不过这里为了记录重入的次数,对 state
共享资源进行了 + acquires
操作,其实就是 +1
操作。
如果都没有成功,那此时则获取锁失败,返回 false
2.3 FairSync下的 tryAcquire
在 FairSync
类下的 tryAcquire
方法中,和前面 NonfairSync
类似,但不同的是,在获取到锁时,也就是拿到 state
等于 0
,进行修改资源时,多了步 hasQueuedPredecessors
的判断:
下面可以进到 hasQueuedPredecessors
的方法中,可以看到是由 AQS
提供的方法,主要就是判断当前节点线程的前面是否还有等待的线程,因为 FairSync
实现的是公平锁的原则,如果当前线程前面还有等待线程,则获取锁资源也轮不到自个,让前面的老大先来:
hasQueuedPredecessors
方法理解后,其余的逻辑则和 NonfairSync
中的一致了。
2.4 tryRelease
到这里已经了解到了tryAcquire
尝试获取资源的逻辑,上面提到了两个重要方法,还有一个 tryRelease
没有分析逻辑,还是首先看 Sync
类中是否有重写该方法:
通过源码可以看到,在 Sync
类中就已经对 tryRelease
进行了重写,而 NonfairSync
和 FairSync
中都没有重写该方法,那释放资源就是走的 Sync
类下的 tryRelease
方法:
在该方法中,可以看到首先还是获取到了 AQS
中的 state
共享资源,然后对该资源进行 - releases
(默认releases
为1
,下面会提到 )操作,其实就是 -1
操作:
接着判断了下,如果当前线程不是持有锁线程,就抛出异常,也好理解,没有持有锁的线程跑过来释放锁,那肯定有问题了呀。
接着再进行判断 state
是不是等于 0
,上面讲到在锁重入的情况下,记录重入的次数是对 state
进行 +1
操作,而这边又对 state
进行 -1
操作,如果减到最后 state
有成了最初的 0
,那不就是重入的锁和当前持有的锁都释放完了吗,这个时候就可以将持有锁的线程置为空了,并修改最新的 state
:
看到这里就会发现获取锁和释放锁,无非就是对 AQS
中的共享资源进行操作。理解了这两大核心的方法后,下面就可以看如何运用在 ReentrantLock
中的了。
三、lock.lock()
在 ReentrantLock
中,需要获取锁时,直接使用 lock.lock()
即可,那 lock.lock()
到底做了什么呢,点到该方法中,可以看到是调用的 Sync
的 lock
方法,而 Sync
中的lock
方法是抽象方法,具体实现肯定在子类的 NonfairSync、 FairSync
中。
3.1 NonfairSync.lock()
首先看点 NonfairSync
非公平锁中的 lock
方法,直接进行了将 AQS
中的共享资源 state
由 0
改为 1
,如果修改成功,根据上面分析的结论不就是获取锁成功了吗,可以将AQS
中的独占线程设为自己了。但是如果其他线程修改成功了,这里使用 CAS
就会修改失败,因此就会进到 acquire
方法,注意这里传递的参数默认就是 1
,对应着前面括号中的说明:
而 acquire
方法,就是 AQS
中的独占模式获取同步资源的逻辑,会调用当前方法的 tryAcquire
尝试获取资源,如果获取不到,则加入到 AQS
的阻塞队列并阻塞挂起线程。
关于acquire
方法的源码解读可以参考文章开始的链接中对 AQS
源码的解读。
3.2 FairSync.lock()
在 FairSync
公平锁中,由于需要遵循先进先出的原则,这里没有直接已粗暴的形式对 state
进行修改,而是直接调用了 AQS
中的 acquire
方法,而 acquire
方法又会调用当前类的 tryAcquire
获取资源。
但在当前类的 tryAcquire
方法中,如果获取到了资源,会接着进行判断当前线程的前面是否还有等待的线程,如果有则让出来让别人获取资源,因此就遵循了公平锁的原则,注意这里传递的参数默认就是 1
,同样对应着前面括号中的说明:
同样 tryAcquire
尝试获取资源,如果获取不到,则加入到 AQS
的阻塞队列并阻塞挂起线程。
四、lock.unlock()
上面了解到了 lock
的逻辑,既然上锁了肯定需要解锁,下面点到 unlock()
方法中,可以看到直接使用了 Sync
的 release
方法释放资源,其实是 AQS
中的 release
方法,注意这里传递的参数默认就是 1
,同样对应着前面括号中的说明:
在 AQS
的release
方法中,首先会调用 Sync
类的 tryRelease
释放资源,然后对已阻塞的线程进行唤醒:
关于release
方法的源码解读可以参考文章开始的链接中对 AQS
源码的解读。
五、总结
通过阅读 ReentrantLock
的源码可以发现,大量依赖于 AQS
中提供的方法,所以在阅读前一定要理解下 AQS
的作用和功能。
Condition 源码解读
一、Condition
在并发情况下进行线程间的协调,如果是使用的 synchronized
锁,我们可以使用 wait/notify
进行唤醒,如果是使用的 Lock
锁的方式,则可以使用 Condition
进行针对性的阻塞和唤醒,相较于 wait/notify
使用起来更灵活。那 Condition
是如何实现线程的等待和唤醒的呢,本篇文章带领大家一起解读下 Condition
的源码。
在进行源码分析前,先回顾下 Condition
是如何使用的,例如下面一个案例:
public class Test {
public synchronized static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("线程1开始等待!");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1被唤醒继续执行结束!");
lock.unlock();
}, "1").start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
System.out.println("开始唤醒线程!");
condition.signal();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2执行结束!");
lock.unlock();
}, "2").start();
}
}
运行之后,可以看到下面日志:
由于在第一个线程中,使用的 condition.await()
因此当前线程会被阻塞挂起,而第二个线程,在 1s
后进行了 condition.signal()
操作,因此第一个线程会被唤醒继续执行。这里细心的小伙伴应该可以发现,第一个线程阻塞时锁并没有释放,而第二个线程在1s
后也成功拿到锁了,所以表明在 condition.await()
时会自动释放当前锁,这点和 wait
相同,在第二个线程进行了 condition.signal()
操作,第一个线程并没有继续向下执行,而是等待第二个线程处理完才会继续执行,由此可以表明被唤醒的线程会重新获取锁,成功获取锁后继续执行。
下面通过源码看下 Condition
是如何实现的等待唤醒。
二、Condition 源码解读
2.1. lock.newCondition() 获取 Condition 对象
首先看下在使用 lock.newCondition()
获取一个Condition
对象时,具体做了什么,这里以 ReentrantLock
为例,进入到 ReentrantLock
的 newCondition()
方法中,又执行了 Sync
的 newCondition()
方法,再进去就会发现其实是 new
了一个 ConditionObject
类对象:
下面点到这个类中,可以看到其实是 AQS
下的一个子类:
2.2. condition.await() 阻塞过程
了解到 Condition
的对象后,可以看到是 AQS
下的一个子类,那下面其他的方法也肯定依赖于 AQS
,下面看下 condition.await()
方法,点到 await()
方法中:
其中 addConditionWaiter()
则是将自己加入到 AQS
的队列中,并获取到当前线程所在的 Node
,这里注意下 Node
的状态是 Node.CONDITION
也就是 -2
,后面会依赖于该状态。
下面再回到 await()
方法继续向下看,接着使用了 fullyRelease
方法传入了当前的 Node
,这里的 fullyRelease
方法主要做了释放当前线程锁的操作,可以看到又调用了 AQS
的 release
进行释放资源,也就是释放了当前所持有的锁。
下面继续回到 await()
方法中,当释放锁后,下面进入到了 while
循环中,通过查看 isOnSyncQueue
方法,可以看到是符合while
的条件也就可以进入到循环中:
在循环中可以明显的看到 LockSupport.park(this)
,将当前线程进行了阻塞。
2.3. condition.signal() 唤醒过程
上面已经看到线程被阻塞了,如果需要被唤醒则需要通过condition.signal()
,这个方法是如何唤醒的呢?
下面来到 AbstractQueuedSynchronizer
类的 signal()
方法中:
主要执行了 doSignal
方法,再点到 doSignal
中,可以看到这里开启了一个循环,对链表的每一个元素都进行了 transferForSignal
操作,这里也比较好理解,就是要唤醒等待中的线程。
下面点到 transferForSignal
中,看下对每个 Node
都做了什么操作。点进去之后也比较好理解,如果状态是 Node.CONDITION
也就是 -2
,刚才在解读 await
方法时就提到这个状态了,这里正好形成了呼应,下面有个非常显眼的操作 LockSupport.unpark(node.thread)
直接唤醒了目标线程。也就是唤醒了 2.2
中的最后一步操作。
2.4. condition.await() 被唤醒后
当 await()
方法中的 LockSupport.park(this)
被唤醒后,继续向下执行,下面会判断下当前线程有没有被打断,如果没被打断则 break
终止循环继续执行。
下面会使用 AQS
的 acquireQueued
方法,将先进入队列的线程进行抢占锁资源,如果成功获取锁后就会继续执行,如果抢占失败则继续被挂起阻塞。
三、总结
通过上面的源码分析,应该对 Condition
有了新的理解和掌握,细心地小伙伴应该可以发现在源码中好多地方都使用了 CAS
,因此当竞争资源非常激烈时, Lock
的性能要远远优于 synchronized
。
CopyOnWriteArrayList
一、CopyOnWriteArrayList 源码解读
在 JUC
中,对于 ArrayList
的线程安全用法,比较推崇于使用 CopyOnWriteArrayList
,那 CopyOnWriteArrayList
是怎么解决线程安全问题的呢,本文带领大家一起解读下 CopyOnWriteArrayList
的源码,主要对几个常用的函数进行讲解。
在进行 CopyOnWriteArrayList
的源码讲解之前,先看下同样实现了线程安全的 Vector
,很多文章都说不推荐使用 Vector
,其主要原因是性能太差了,那性能为什么这么差呢?可以看下 Vector
add
和 get
的源码:
Vector
的添加和读取操作都被加上了 synchronized
锁,当并发情况下,因为锁的存在相当于变成了单线程的操作,所以效率肯定低,同样这样的优点就是保证了数据的唯一性,不会读取到脏数据。
下面再看下 CopyOnWriteArrayList
是如何解决并发问题的呢。
首先看下 CopyOnWriteArrayList
的全局变量有哪些:
其中 lock
锁就是每次在做写操作时,锁的句柄,array
就是具体存储数据的数组,注意这里的 array
被 volatile
所修饰,因此可以在并发情况下实现数据的可见性。
当 new
创建了一个 CopyOnWriteArrayList
时,如果是使用无参的构造函数,则将 array
的长度默认成 0
,创建了一个空的数组。
在使用 add
添加数据时,先使用 lock
上锁,并获取到当前的 array
数组,然后对 array
进行 copyOf
,新的数组的长度是之前的长度 +1
,这样才能存放当前新的值,将新值填充后,再替换掉旧的 array
数组后,释放当前锁。
在使用 get
获取指定下边数据时,直接对当前的 array
进行操作:
在进行 remove
删除时,先使用 lock
上锁,然后再获取当前的 array
数组,如果传入的 index
正好是最后一个,那么 numMoved
计算出来就是 0
,则使用 copyOf
,长度进行 -1
去除最后一个数据。否则传入的不是最后一个,先声明一个新的 array
数组,数组的长度就是旧的 array
的 len - 1
,再将 0
到 index
的数据 arraycopy
至新的 array
数组,然后再将 index + 1
后的再 arraycopy
至新的 array
数组,最后将新的 array
数组替换旧的,然后释放锁。
二、总结
- 当
new
新建一个CopyOnWriteArrayList
后会生成一个数组array
来存放添加的内容,如果是无参的构造函数,则array
的长度为0
,添加数据时再进行扩容。同时会声明一个ReentrantLock
锁。 - 当进行
add
操作时,先进行上锁,然后对当前的array
进行copyOf
,并且新的长度是之前的长度+1
,这样才能存放当前新的值,将新值填充后,再替换掉旧的array
数组后,释放当前锁。 - 当使用
get
获取数据时,无需上锁,直接读取当前array
数组的指定位置。 - 当使用
remove
时,同样先进行上锁,然后再获取当前的array
数组,如果传入的index
正好是最后一个,则使用copyOf
,长度进行-1
,否则的话先声明一个新的array
数组,现将0
到index
的数据arraycopy
至新的array
数组,然后再将index + 1
后的再arraycopy
至新的array
数组,最后将新的array
数组替换旧的,然后释放锁。
读下来之后可以感觉出来 CopyOnWriteArrayList
的源码非常容易理解和阅读,同时我们也可以看出一些问题,CopyOnWriteArrayList
实现了写写隔离,但读读是可以共享的,这就有可能出现当某个数据再修改时,读进行了操作,导致读取到的还是旧的数据。还有就是每次写操作都对数组进行 Copy
,假如数据量非常大的情况下,进行 Copy
消耗的资源则会进行 x 2
,因此使用 CopyOnWriteArrayList
时,需要考虑下自己的数据量以及读写的频次。