JUC三大辅助类
计数器(CountDownLatch)
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减 1 的操作,使用await方法等待计数器不大于 0 ,然后继续执行await方法之后的语句。
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞,其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。当计数器的值变为 0 时,因await方法阻塞的线程会被唤醒,继续执行。
/**
* 6个同学陆续离开教室后值班同学才可以关门
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception {
//定义一个数值为 6 的计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
//创建 6 个同学
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
if (Thread.currentThread().getName().equals("同学6")) {
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "离开了");
//计数器减一,不会阻塞
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}, "同学" + i).start();
}
//主线程await休息
System.out.println("主线程睡觉");
countDownLatch.await();
//全部离开后自动唤醒主线程
System.out.println("全部离开了,现在的计数器为" + countDownLatch.getCount());
}
}
循环屏障(CyclicBarrier)
CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()
进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。CyclicBarrier就象它名字的意思一样,可看成是个循环障碍, 所有的线程必须到齐后才能一起通过这个障碍。CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
/**
* 集齐 7 颗龙珠就可以召唤神龙
*/
public class CyclicBarrierDemo {
//定义神龙召唤需要的龙珠总数
private final static int NUMBER = 7;
public static void main(String[] args) {
//定义循环栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () ->{
System.out.println("集齐" + NUMBER + "颗龙珠,现在召唤神龙!!!");
});
//定义 7 个线程分别去收集龙珠
for (int i = 1; i <= 7; i++) {
new Thread(()->{
try {
if(Thread.currentThread().getName().equals("龙珠3")){
System.out.println("龙珠3,发生抢夺,延时");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "收集到了!!!!");
}else{
System.out.println(Thread.currentThread().getName() + "收集到了!!!!");
}
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}
}, "龙珠" + i).start();
}
}
}
信号量(Semaphore)
Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下:availablePermits //函数用来获取当前可用的资源数量park.acquire(); //申请资源park.release();// 释放资源
/**
* 抢车位, 6辆车竞争 1个停车位
*/
public class SemaphoreDemo {
public static void main(String[] args) throws Exception {
//定义 1 个停车位
Semaphore semaphore = new Semaphore(1);
//模拟 6 辆车停车
for (int i = 1; i <= 6; i++) {
Thread.sleep(100);
//停车
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "找车位ing");
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "汽车停车成功 !");
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "离开车位");
semaphore.release();
}
}, "汽车" + i).start();
}
}
}
并发队列
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue
为代表的高性能队列,一个是以BlockingQueue
接口为代表的阻塞队列,无论哪种都继承自Queue。
ConcurrentLinkedDeque
ConcurrentLinkedQueue 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则,且不允许null元素。ConcurrentLinkedQueue重要方法:add 和offer()
都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)poll() 和peek()
都是取头元素节点,区别在于前者会删除元素,后者不会。
BlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空(即,队列为空,获取元素会被阻塞)。
- 当队列满时,存储元素的线程会等待队列可用(即,队列满了,添加元素会被阻塞)。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。
被阻塞的情况主要有如下两种场景:
- 当队列满了的时候进行入队列操作
- 当队列空了的时候进行出队列操作
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞(线程被挂起),除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
BlockingQueue详解
概述
在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。下面详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。
BlockingQueue,阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
从上图可知,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
- 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
- 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)
ArrayBlockingQueue(常用)
由数组结构组成的有界阻塞队列
ArrayBlockingQueue是一个有边界的阻塞队列,即容量有限,它的内部实现是一个数组。必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变,以先进先出的方式存储数据。ArrayBlockingQueue的示例:
public static void main(String[] args) {
ArrayBlockingQueue<String> arrays = new ArrayBlockingQueue<>(3);
arrays.add("张三");
arrays.add("李四");
arrays.add("李四");
// 添加阻塞队列
try {
arrays.offer("王五", 1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LinkedBlockingQueue(常用)
由链表组成的有界阻塞队列(不指定默认为无界)
LinkedBlockingQueue阻塞队列大小的配置是可选的,如果初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE
的容量 。它的内部实现是一个链表,以先进先出的方式存储数据。
LinkedBlockingQueue的示例:
public static void main(String[] args) {
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.add("张三");
linkedBlockingQueue.add("李四");
linkedBlockingQueue.add("李四");
System.out.println(linkedBlockingQueue.size());
}
PriorityBlockingQueue
支持优先级排序的无界阻塞队列
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者 。因此使用的时候要特别注意, 生产者生产数据的速度绝对不能快于消费者消费数据的速度 ,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是 公平锁 。
DelayQueue
使用优先级队列实现的延迟无界阻塞队列
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
SynchronousQueue
不存储元素的阻塞队列,也即单个元素的队列
SynchronousQueue队列一种无缓冲的等待队列,内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
声明一个SynchronousQueue有两种不同的方式:
公平模式和非公平模式的区别:
- 公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
- 非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
LinkedTransferQueue
由链表组成的无界阻塞队列
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。
LinkedBlockingDeque
由链表组成的双向阻塞队列
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,这里的阻塞一般有两种情况:
插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再将该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出InterruptedException异常
读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
BlockingQueue模拟生产者与消费示例
生产
public class ProducerThread implements Runnable {
private BlockingQueue queue;
private volatile boolean flag = true;
private static AtomicInteger count = new AtomicInteger();
public ProducerThread(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
System.out.println("生产线程启动...");
while (flag) {
System.out.println("正在生产数据....");
String data = count.incrementAndGet() + "";
// 将数据存入队列中
boolean offer = queue.offer(data, 2, TimeUnit.SECONDS);
if (offer) {
System.out.println("生产者,存入" + data + "到队列中,成功.");
} else {
System.out.println("生产者,存入" + data + "到队列中,失败.");
}
Thread.sleep(1000);
}
} catch (Exception e) {
} finally {
System.out.println("生产者退出线程");
}
}
public void stop() {
this.flag = false;
}
}
消费
public class ConsumerThread implements Runnable {
private BlockingQueue<String> queue;
private volatile boolean flag = true;
public ConsumerThread(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println("消费线程启动...");
try {
while (flag) {
System.out.println("消费者,正在从队列中获取数据..");
String data = queue.poll(2, TimeUnit.SECONDS);
if (data != null) {
System.out.println("消费者,拿到队列中的数据data:" + data);
Thread.sleep(1000);
} else {
System.out.println("消费者,超过2秒未获取到数据..");
flag = false;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("消费者退出线程...");
}
}
}
测试:指定大小的LinkedBlockingQueue
public class ProductAndConsumerTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
ProducerThread producerThread1 = new ProducerThread(queue);
ProducerThread producerThread2 = new ProducerThread(queue);
ConsumerThread consumerThread1 = new ConsumerThread(queue);
Thread t1 = new Thread(producerThread1);
Thread t2 = new Thread(producerThread2);
Thread c1 = new Thread(consumerThread1);
t1.start();
t2.start();
c1.start();
// 执行10s
Thread.sleep(10 * 1000);
producerThread1.stop();
producerThread2.stop();
}
}