写在前面
之前有写过AQS的相关博文,那么本文就来写写使用AQS相关的同步组件。
AQS是java中提供用来构建同步组件的基础框架,它提供了基本的同步状态管理,线程阻塞、排队、唤醒机制。从而方便用户来自定义同步组件。
在java并发包中,也有不少组件使用了AQS。
下面来简单的介绍一下使用到AQS的常用的同步组件。
这里我们按照AQS的资源共享方式来分类:
- 独占式:ReentrantLock
- 共享式:Semaphore、CountDownLatch、CycliBarrier
- 独占+共享式:读写锁
ReentrantLock
重入锁
见名知意,该组件是一个锁(它实现了Lock接口),它支持在已经获得锁对象的情况下,继续获取锁。
(Synchronized是可以隐式的重入锁的)
所以ReentrantLock在持有锁对象时,再次调用lock()方法获取锁而不会被阻塞。
至于如何实现,我们来看源码,这里我们先看非公平锁的实现,至于公平和非公平的区别,我们后面再看。
下面是ReentrantLock源码中集成的AQS非公平锁的获取:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果状态为0,则意为无锁状态,可以获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果有锁,判断持锁线程是不是自己
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里是ReentrantLock重写了AQS的tryAcquire方法,在其中默认调用了nonFairlyAcquire方法。
- 如果同步状态为0,则意为无锁状态,可以获取
- 如果有锁,判断持锁线程是不是自己。如果是自己,则继续增加同步状态变量。
这样一看,逻辑就很清晰了。
再来看看tryRelease方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
emmmm,和获取是对应的,要判断持有锁的是不是自己,然后减小同步状态变量。
总结一下:
- 对于重入锁,它的同步状态变量取值是0-正无穷,其中0为无锁,非0为有多少把锁。
- 加锁多少次,就得释放锁多少次才能完全成功释放锁。
- 只有当前持有锁的线程才能再次获取锁。
下面我们再来看看公平锁与非公平锁的概念以及在ReentrantLock中它是如何体现的。
(图片来源:https://www.cnblogs.com/a1439775520/p/12947010.html)
上面是ReentrantLock的类结构图。
我们只需要看看NonFairSync和FairSync的tryAcquire方法,就能够明白公平/非公平的区别。
下面是公平获取的源码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&//**看这里**
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
和上面的nonfairTryAcquire方法对比,我们发现只有一个地方不一样,就是在c==0时,多判断了一个hasQueuePredecessors方法。
![](/qrcode.jpg)
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
看最后一行return语句:
h!=t
,如果相等,则意味队列为空,如果不相等,则意味至少有两个不同的节点h.next==null
如果不为空,还有后继节点s.thread != Thread.currentThread()
,如果相等,则意味着,自己的前驱节点就是头结点,已经轮到自己来竞争了,则当然可以竞争
总的来说,该方法就是判断,队列是否为空,或者队列不为空时,自己的前驱是否是头节点,从而边可以进行竞争。除此之外的情况,都返回true,不能竞争。
总结一下,判断队列中是否有其他线程等待,如果等待,乖乖排队,否则直接竞争。
我举这样一个例子来说明公平锁与非公平锁。
- 公平锁:食堂打饭,一开始每人,来打饭的人都直接去打饭。当人多了开始排队。新来的人发现排队了,老实的直接去乖乖排队。
- 非公平锁:食堂打饭,一开始没人,来打饭的人都直接去打饭。当人多了开始排队。此时新来的人发现排队了,它不乖,插队到窗口位置抢饭。如果抢成功了,就吃上饭了,如果失败了,就被别人指责(虚构),乖乖到队尾排队。
当然,二者各有优劣。公平锁能够保证公平,保证FIFO。
非公平锁的吞吐量大,因为不用唤醒后继节点,节省了不少的开销。
但是非公平锁可能会导致饥饿,当你是普通用户去银行办理业务,排着号,然后前面老是有vip插队,你说你饥饿吗?我就遇到过,真是tmd烦死了。
Semaphore
一般称之为信号量,它主要是实现,允许对一个资源,允许同时N个线程访问。
直接看源码吧,Semaphore的源码还是比较少的。
在Semaphore中也是有公平和非公平概念的,这里似乎就不太好说锁这个东西了。
来看其公平的获取:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
同ReentrantLock,公平与非公平差异不大。
这里的逻辑也是很简单,如果hasQueuedPrecessors,就失败。
如果当前同步状态减参数(一般是1)小于0,就代表获取失败,放弃获取,直接返回remaining(负数)。(同步状态变量大于0则代表还允许线程获取资源)
Semaphore调用的是AQS中的模板方法,当获取失败时,就会加入到同步队列中,然后被阻塞。
然后关于释放,这里的源码为:
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
当同步状态变量>=0时,才会调用AQS的releaseShared模板方法。
总的来说,Semaphore限制了访问同步资源的线程个数。
CountDownLatch
它的作用是,让若干个线程 等待 若干个线程。
一般我们要实现一个线程等待另外一个线程,我们经常使用锁变量的wait()和notify()方法,
但那只能实现,当我被等待的线程notify()了,就马上有一个wait()的线程被唤醒。同时还伴随着锁的释放和获取,这就达不到我们的效果了。
我们想要的是,一群线程 等待 整整一群线程做完,才继续进行。
于是,CountDownLatch帮我们实现了这个想法。
CountDownLatch countDownLatch = new CountDownLatch(3);
我们在主线程中设置CountDownLatch的计数器为3,
调用其他线程,之后await(),进入阻塞,此时当其他线程执行countDown方法时,就会将CountDownLatch的计数器减1,当减到0时,则主线程会被唤醒。
这里用这样一个例子来演示:
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
run..run..run..run..run..run..run..run..run..run..end
这里说明一下,countdownLatch是一次性的,计数器为0了就是为0了,不能重置。
其次,等待的若干线程满足条件之后继续执行,作为条件的被等待的线程,不管,可以继续执行或者等待,无所谓。
CountDownLatch的两个经典用法:
- 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
- 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。(同时起跑)
(关于CountDownLatch的源码部分,之后如果有需要的话再进行补充)
CycliBarrier
循环栅栏
CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
它是一个屏障,它的功能是,保证在一个位置,足够数量的线程到达这个位置之后,大家才能一起出发(类似于上面的同时起跑)。早到的等待,晚到的不急。
它需要使用默认的构造方法指定屏障拦截的线程数量
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
这里的parties指的是,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
而Runnable指的是,达到值时,先自动执行这个方法。
应用场景:
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。
当线程到达屏障时,执行await()方法,进入休眠,等待其他线程。
await方法中调用了dowait方法,源码如下:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;//将计数器值减1 count是CycliBarrier的一个变量,就是计数器
//当计数器值为0时,则可以放行
if (index == 0) {
// tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//这里是关键
//将count重置
//唤醒之前等待的线程
//下一波执行开始
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
通过我的注释可以看到,这里最重要的是nextGeneration方法:
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
可以看到,该方法是重置计数器,并且唤醒其他所有等待线程。
于是,我们便知道,这里和AQS使用CLH锁是不一样的。
前面的线程被阻塞。
每次都是最后一个线程唤醒其他线程。
这也是因为使用了ReentrantLock和condition的缘由。
对比CountDownLatch,CycliBarrier更像是一个阀门,他是保证所有线程都到达某各自的一个指定位置。
而CountDownLatch更关注于条件这个概念。
读写锁
在平常我们都说28原则,读8写2,且读安全,写不安全
于是读采用共享,写采用独占。
这里有点累了,后面再写。