写在前面
本篇对JDK中并发包下常用的类进行一个总结,将会对常用的BlockingQueue阻塞队列、CountDownLatch、CyclicBarrier以及Semaphore进行总结。
阻塞队列
BlockingQueue是Java的JUC包下提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的身影。
队列类型
- 无界队列:几乎可以无限增长
- 有界队列:定义了最大容量
常见的阻塞队列
- ArrayBlockingQueue 由数组支持的有界队列
- LinkedBlockingQueue 由链接节点支持的可选有界队列
- PriorityBlockingQueue 由优先级堆支持的无界优先级队列
- DelayQueue 由优先级堆支持的、基于时间的调度队列
以上是常见的四种阻塞队列,相对来说比较常用的是ArrayBlockingQueue和LinkedBlockingQueue。接下来将以ArrayBlockingQueue为例,对其原理进行简单的分析总结。
ArrayBlockingQueue
添加元素API:
方法 | 说明 |
---|---|
add() | 如果插入成功则返回 true,否则抛出 IllegalStateException 异常 |
put() | 将指定的元素插入队列,如果队列满了,那么会阻塞直到有空间插入 |
offer(E e) | 如果插入成功则返回 true,否则返回 false |
offer(E e,long timeout,TimeUnit unit) | 尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入 |
查询元素API:
方法 | 说明 |
---|---|
take() | 获取队列的头部元素并将其删除,如果队列为空,则阻塞并等待元素变为可用 |
poll(long timeout, TimeUnit unit) | 检索并删除队列的头部,如有必要,等待指定的等待时间以使元素可用,如果超时,则返回 null |
以上并不是全部的API只是一些关键主要的API方法。
add()方法和offer()方法
从源码中可以看到add()方法最终调用的还是offer()方法,只不过add()方法如果队列满了添加元素失败会抛出异常,而offer()方法最终返回的是true和false。
put()方法和offer(E e,long timeout,TimeUnit unit)方法
从源码可以看到put()方法与offer()方法的区别在于,offer()使用的是lock.lock() 进行加锁,而put()使用的是lock.lockInterruptibly(),区别在于lockInterruptibly会判断如果线程被中断了,会抛出中断异常,而lock则不会,如下图所示:
还有一个地方,可以发现put()方法在数组容量满的时候,会使用notFull.await()进行阻塞。notFull和notEmpty在构造函数中初始化,它是lock的条件等待状态,在前面的文章中讲过,AQS的Node节点有包含了几种状态其中的一种表示的是条件等待。
当队列不满的时候,就会跳出while循环,进入enqueue()方法,使用notEmpty.signal()唤醒等待的线程。
offer(E e, long timeout, TimeUnit unit)与put方法类似,只不过在等待条件中增加了等待的时间。
以上是对添加元素的源码进行粗略的分析查看,会发现BlockingQueue底层依赖是使用ReentrantLock来进行加锁。同时在put()和offer()方法如果队列满了则使用了条件等待进行阻塞。那么在获取元素的时候如果队列空了是否也会使用条件等待进行阻塞操作?
take()方法和poll()方法
同样的对比一下poll()、take()和带参数的poll()方法,可以发现其中的区别。
同样在出队列dequeue()方法中也会对线程进行唤醒操作。
Semaphore
Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目,底层依赖AQS的状态State,是在生产当中比较常用的一个工具类。
构造方法
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
复制代码
- permits 表示许可线程的数量
- fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
重要方法
public void acquire() throws InterruptedException
public void release()
tryAcquire(int args,long timeout, TimeUnit unit)
复制代码
- acquire() 表示阻塞并获取许可
- release() 表示释放许可
代码示例
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i=0;i<10;i++){
new Thread(new Task(semaphore,i)).start();
}
}
static class Task extends Thread{
Semaphore semaphore;
public Task(Semaphore semaphore,String tname){
this.semaphore = semaphore;
this.setName(tname);
}
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+":aquire() at time:"+System.currentTimeMillis());
Thread.sleep(1000);
semaphore.release();
System.out.println(Thread.currentThread().getName()+":aquire() at time:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
查看输出结果会发现启动了10个线程,每隔5s输出一次,每次只输出2个线程的打印信息。
Semaphore构造函数
在创建Semaphore对象的时候需要传入参数,这个参数最终设置的是AQS里面的state同步状态,表示最大的并行度。
Semaphore也分为公平模式与非公平模式,默认使用的是非公平模式。
acquire()方法源码分析
acquire()提供了两个方法一个是无参,一个是有参。调用acquire()方法,就是每次去获取信号量,state就会减1。
acquire()调用的是sync.acquireSharedInterruptibly()方法。
首先会先调用tryAcquireShared(arg)尝试获取信号量。
基于公平模式下,首先会判断是否在AQS的队列里面,其次获取state同步状态的值,也就是在初始化的时候,会设置state表示信号量的数量。使用当前剩余的信号量数量available减去请求需要的信号量acquire,如果小于0,则说明剩余信号量不足以请求所需要的信号量,则会执行doAcquireSharedInterruptibly(arg)。 如果大于0,则直接CAS更新state的值。
这里面的逻辑与ReentrantLock的acquireQueued方法类似。首先会创建一个节点并加入到CLH队列中,但此时的节点为SHARED(共享模式),同样,在阻塞之前会先取出当前节点的前驱节点,如果是头节点,再尝试获取一次信号量,如果获取成功,则进入setHeadAndPropagate方法。否则就会进入shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()判断,再前面介绍过这两个方法,在此不做赘述。这两个方法主要是将node节点的状态改为SIGNAL(可唤醒)且阻塞线程。
setHeadAndPropagate方法主要是设置队列头,并检查后继者是否可能在共享模式下等待,如果传播 > 0 或设置了PROPAGATE状态,则传播。设置对象头,再之前的文章中也有介绍,即将node.thread和node.prev都设置为null。设置队列头后,则进入doReleaseShared()方法。
这里面主要是实现唤醒下一个节点并确保传播。也就是,从头节点开始,如果遇到阻塞的线程(在shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()中阻塞,且状态也是SIGNAL),即ws会是SIGNAL则将状态改为0,修改失败,continue继续循环,修改成功则唤醒线程,遇到了ws==0的节点状态则修改为PROPAGATE(传播模式),修改失败则continue继续循环,直到修改成功。如果头节点不变(多个线程访问,可能会导致h头节点改变),则跳出循环。 图例:
首先,t0和t1两个线程获取到信号量,此时state值为0,t2,t3,t4线程需要排队,创建node的时候waitStatus的状态默认为0,在入队的时候,会去修改前驱节点的状态为SIGNAL(-1),因此最后一个节点的waitStaus的状态是0。
接着t0释放了信号量,会去唤醒下一个节点也就是t2线程,同时将waitStatus改为0(也就是doReleaseShared()方法的第一个if判断CAS操作成功并unparkSuccessor()唤醒线程)。
紧接着,会判断头节点的waitStatus为0,则CAS操作修改为-3(即doReleaseShared()方法的else if判断)。
release()方法源码
同样release()也提供了两个方法,一个是带参,一个是无参。调用release()方法,就是每次去获取信号量,state就会加1。
release()方法调用的即releaseShared()方法,会先尝试去释放信号量。即CAS操作加会state的值。同样如果成功了,则会调用doReleaseShared()方法唤醒下一个节点并确保是传播状态。
CountDownLatch
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
CountDownLatch原理和Semaphore原理类似,同样是基于AQS,不过没有公平和非公平之分。
代码示例
public class CountDownTest {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
System.out.println("线程1执行结束");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
System.out.println("线程2执行结束");
}
});
System.out.println("等待子线程执行结束");
countDownLatch.await();
System.out.println("所有线程执行结束");
executorService.shutdown();
}
}
复制代码
执行结果
构造方法
同样,CountDownLatch是使用AQS实现的。通过下面的构造函数,你会发现,实际上是把计数器的值赋给了AQS的状态变量state,也就是这里使用AQS的状态值来表示计数器值。
await()方法
当线程调用CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回:当所有线程都调用了CountDownLatch对象的countDown方法后,也就是计数器的值为0时;其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程就会抛出InterruptedException异常,然后返回。
从源码可以看出,该方法的特点是线程获取资源时可以被中断,并且获取的资源是共享资源。acquireSharedInterruptibly首先判断当前线程是否已被中断,若是则抛出异常,否则调用sync实现的tryAcquireShared方法查看当前状态值(计数器值)是否为0,是则当前线程的await()方法直接返回,否则调用AQS的doAcquireSharedInterruptibly方法让当前线程阻塞。另外可以看到,这里tryAcquireShared传递的arg参数没有被用到,调用tryAcquireShared的方法仅仅是为了检查当前状态值是不是为0,并没有调用CAS让当前状态值减1。
countDown()方法
从源码可以看出,CountDownLatch的countDown()方法委托sync调用了AQS的releaseShared方法,接着会调用tryReleaseShared()方法,该方法中,首先判断如果当前状态值为0则直接返回false,从而countDown()方法直接返回,接着,使用CAS将计数器值减1, CAS失败则循环重试,否则如果当前计数器值为0则返回true,返回true说明是最后一个线程调用的countdown方法,那么该线程除了让计数器值减1外,还需要唤醒因调用CountDownLatch的await方法而被阻塞的线程,具体是调用AQS的doReleaseShared方法来激活阻塞的线程。doReleaseShared()方法在前面有介绍过,不做赘述。
CyclicBarrier
CyclicBarrier是回环屏障的意思,它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await方法后,线程们就会冲破屏障,继续向下运行。
例子:
public class CyclicBarrierTest {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+" step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+" step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+" step3");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+" step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+" step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+" step3");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
executorService.shutdown();
}
}
复制代码
输出结果:
在如上代码中,每个子线程在执行完阶段1后都调用了await方法,等到所有线程都到达屏障点后才会一块往下执行,这就保证了所有线程都完成了阶段1后才会开始执行阶段2。然后在阶段2后面调用了await方法,这保证了所有线程都完成了阶段2后,才能开始阶段3的执行。
最后
自此本篇结束,CyclicBarrier源码待后面有时间在去看。