-
并发线程工具类
-
ThreadLocal类
-
阻塞队列
1. 并发线程工具类
1.1 CountDownLatch
1.1.1 概述
等待多线程完成的CountDownLatch,允许一个或多个线程等待其他线程完成操作。底层依赖AQS实现 【减法器】
1.1.2 案例说明
如门卫关门,教室里面有10个同学,必须所有同学全部出去之后再关门。
1.1.3 案列代码
/*CountDownLatch 【减法计数器】
需求: 如门卫关门,教室里面有10个同学,必须所有同学全部出去之后再关门
分析:10个学生就是10个线程,关门又是一个线程
当10个学生线程都执行完毕再执行关门线程
*/
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
// 创建减法计数器对象
CountDownLatch countDown = new CountDownLatch(10);
long start = System.currentTimeMillis();
// 1. 准备线程任务
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +"出教室成功.");
// 每当一个学生线程执行完毕:countDown就减少一个
countDown.countDown();
}
};
// 2. 循环10次,开启10个学生线程
for (int i = 1; i <= 10; i++) {
new Thread(task,"学生"+i).start();
}
// 当countDown 中的计数没有到0就在这里阻塞,后面的代码不能执行
countDown.await();
// 3. 开启关门线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("关门完毕");
}
}).start();
System.out.println("全部线程执行完毕所花时间:"+(System.currentTimeMillis()-start));
}
}
1.2 CyclicBarrier
1.2.1 概述
上面是一个减法计数器,这个是一个加法计数器
1.2.2 案例说明
现在有这么一个需求,现在要集齐小当家108张梁山好汉可以兑换小霸王学习机
【可以让一批线程之间相互等待,等到某一个临界值的时候,再同时往下执行!】
1.2.3 案列代码
/*CyclicBarrier【加法计数器】
现在有这么一个需求,现在要集齐小当家108张梁山好汉可以兑换小霸王学习机
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建加法计数器对象
CyclicBarrier barrier = new CyclicBarrier(108, new Runnable() {
@Override
public void run() {
System.out.println("小霸王学习机带回家。");
}
});
// 2. 开启108个线程集齐108个梁山好汉
for (int i = 1; i <= 108; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +"被收集到了");
// 每当一个线程被执行完毕就让加法计数器加一次
try {
// 每当一个线程执行到此处就会等待,并且加法计数器加一,当统计的次数达到108时,才会一起向下执行,最后执行加法计数器中的任务
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
},"好汉"+i).start();
}
}
}
1.3 Semaphore【信号量】
1.3.1 概述
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。底层依赖AQS实现,存在公平和非公平两种模式,类似ReentrantLock。
1.3.2 案例说明
比果一个停车厂的停车位是有限的,那么当车位停满之后,其它的车(线程)是不能进来的。只有等有人离开之后才能进来.
1.3.3 案列代码
/*
Semaphore【信号量】
模拟停车场停车,停车场容量为:3
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 创建信号量对象:允许的并发线程数量为3, 模拟停车场中只有3个车位
Semaphore sema = new Semaphore(3);
// 创建线程任务对象
Runnable task = new Runnable() {
@Override
public void run() {
try {
//线程开启之后,acquire():让运行的线程获取运行许可,最多3个
sema.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"进行停车场。");
// 模拟停车时间
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"离开停车场。");
// 当一个线程运行完毕之后,释放一个许可
sema.release();
}
};
// 开启10个线程
for (int i = 1; i <= 10; i++) {
new Thread(task,"车辆"+i).start();
}
}
}
2. ThreadLocal类
2.1 ThreadLocal类简介
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是线程局部变量(thread local variable)。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本(关于ThreadLocal的底层实现,感兴趣的同学可以在网上查资料学习)。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
2.2 ThreadLocal的使用
ThreadLocal用于保存某个线程共享变量,ThreadLocal类中的泛型T代表的就是我们需要保存共享变量的数据类型。
使用ThreadLocal类时,JDK建议把ThreadLocal对象定义为private static来修饰。对于同一个静态的ThreadLocal对象,不同线程只能从中获取、设置和移除自己线程的变量,而不会影响其它线程的变量。
接下来,先了解一下ThreadLocal类提供的几个重要方法:
关于ThreadLocal类提供的方法详解:
2.2.1 T get()方法
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则需要创建并初始化此副本。
2.2.2 protected T initialValue()方法
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(T)方法,则不会在线程中再调用initialValue方法。
如果程序员希望将线程局部变量初始化为null以外的某个值,则必须为ThreadLocal创建子类,并重写此方法。通常,将使用匿名内部类。initialValue的典型实现将调用一个适当的构造方法,并返回新构造的对象。
2.2.3 void remove()方法
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其initialValue。
2.2.4 void set(T value)方法
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于initialValue()方法来设置线程局部变量的值。在程序中一般都重写initialValue方法,以给定一个特定的初始值。
2.2.5 ThreadLocal的使用场合
ThreadLocal使用场合主要解决多线程中因并发产生的数据不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但是大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
案例需求:创建一个Stduent类,要求在不同的线程中设置Student对象的name属性,要求保证各个线程中Student对象的独立性。
使用ThreadLocal之前的线程安全问题
/*
学生类
*/
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 定义线程任务类
*/
public class MyRunableTask implements Runnable{
private Student stu = new Student();
@Override
public void run() {
stu.setName(Thread.currentThread().getName());
// 切换到其他线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出学生姓名
System.out.println(stu.getName());
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 线程安全问题演示
*/
public class ThreadLocalDemo1 {
public static void main(String[] args) {
// 1. 创建线程任务类对象
MyRunableTask task = new MyRunableTask();
// 2. 开启三个线程
new Thread(task,"学生A").start();
new Thread(task,"学生B").start();
new Thread(task,"学生C").start();
}
}
运行结果如图:
原因:三个线程操作了同一个学生对象,操作完成之后,三个线程才去打印了同一个学生对象的姓名属性
解决办法:采用同步来解决,让线程的任务代码放入同步代码块中,让三个线程排队执行。
优化线程人物类代码如下:
/*
定义线程任务类
*/
public class MyRunableTask implements Runnable{
private Student stu = new Student();
@Override
public void run() {
synchronized (this) {
stu.setName(Thread.currentThread().getName());
// 切换到其他线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stu.getName());
}
}
}
运行结果如下:
说明:使用同步代码块解决线程数据污染的安全问题是以时间换安全,以牺牲线程执行所花时间,来换取数据的安全性。
另外一种解决方案:ThreadLocal
ThreadLocal的使用步骤:
-
创建一个继承于ThreadLocal类的子类,该子类需要重写initialValue()方法,在重写方法中返回需要隔离处理的对象(例如Student对象),避免了线程第一次访问get方法返回null的情况。
-
在线程任务类(例如ThreadLocalRunnable 类)中,创建一个私有静态的ThreadLocal对象,用来保存线程间需要隔离处理的对象(例如Student对象)。
-
在线程任务类的run()方法中,通过ThreadLocal类的get()方法获取要操作的对象,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
【示例】线程局部变量使用案例
/*
学生类
*/
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 自定义ThreadLocal类
*/
public class MyThreadLocal extends ThreadLocal<Student>{
// 必须重写initValue()方法
@Override
protected Student initialValue() {
return new Student();
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 定义线程任务类
*/
public class MyRunableTask implements Runnable {
// 定义ThreadLocal属性
private static MyThreadLocal threadLocal = new MyThreadLocal();
@Override
public void run() {
/*
每当一个线程任务执行,就调用一次get()方法,执行MyThreadLocal中的initialValue(),
获取到当前线程的Student对象副本
*/
Student stu = threadLocal.get();
// 设置当前线程获取的Student对象副本的name属性
stu.setName(Thread.currentThread().getName());
// 切换到其他线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出当前线程学生对象副本的姓名
System.out.println(stu.getName());
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 使用ThreadLocal解决线程安全问题
*/
public class ThreadLocalDemo2 {
public static void main(String[] args) {
// 1. 创建线程任务类对象
MyRunableTask task = new MyRunableTask();
// 2. 开启三个线程
new Thread(task,"学生A").start();
new Thread(task,"学生B").start();
new Thread(task,"学生C").start();
}
}
运行结果如图:
ThreadLocal原理分析图解:
使用ThreadLocal的优点:
通过运行结果可知,在“学生A”、“学生B”、“学生C”这三个线程中,Student对象的name在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,也兼顾数据的安全性。
总结:实现了并发线程之间数据的隔离。
2.2.6 ThreadLocal和Synchonized对比
ThreadLocal和synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的共享。而synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。synchronized用于实现同步机制,比ThreadLocal更加复杂。
3. 阻塞队列
3.1 概述
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。
3.2 认识BlocakingQueue
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
先进先出(FIFO):先插入队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒),下面两幅图演示了BlockingQueue的两个常见阻塞场景:
如上图所示:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
阻塞队列原理分析图解:
3.3 BlockingQueue的核心方法
3.3.1 方法总结
3.3.2 放入数据
-
offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
-
offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
-
put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
3.3.3 获取数据
-
poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
-
poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
-
take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,则进入阻塞状态,直到BlockingQueue有新的数据被加入;
-
drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
3.4 常见的BlockingQueue
在了解了BlockingQueue的基本功能后,让我们来看看BlockingQueue家庭大致有哪些成员?
3.4.1 ArrayBlockingQueue
3.4.1.1 ArrayBlockingQueue 使用场景
-
先进先出队列(队列头的是最先进队的元素;队列尾的是最后进队的元素)
-
有界队列(即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞进队操作;容量空,则阻塞出队操作)
-
队列不支持空元素
3.4.1.2 简单案例
/*
ArrayBlockingQueue
说明: 底层数据结构为:数组的阻塞队列
特点: 1. 先进先出
2. 有界
3. 不能存null
*/
public class BlockingQueueDemo1 {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<>(10);
// 开启一个线程:生产者线程 向abq中存入10个元素
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
boolean offer = abq.offer(i);
System.out.println(offer);
}
}
}).start();
// 使用主线程模拟消费者线程: 不断从abq中取出元素,如果abq是空的,那么消费者线程阻塞
while (true) {
// 从abq中不断取出元素,若abq为空,则进入阻塞状态直到新元素被添加到abq中
Integer take = abq.take();
System.out.println(take+"被取出");
}
}
}
3.4.2 LinkedBlockingQueue
基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
下面的代码演示了如何使用BlockingQueue:
/*
LinkedBlockingQueue
说明: 基于链表的阻塞队列
特点: 1. 可以无界也可以有界
2. 无界阻塞队列不会阻塞生产者线程,但是慎用,因为可能会占满整个内存
*/
public class LinkedBlockingQueueDemo1 {
public static void main(String[] args) throws InterruptedException {
// 1. 创建无界的阻塞队列
LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<>();
// 2. 开启一个生产者线程:生产10个数据存入lbq中
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
boolean offer = lbq.offer(i);
System.out.println(offer);
}
}
}).start();
// 使用主线程模拟消费者线程,当lbq中是空的时,消费者线程会阻塞
for (int i = 1; i <= 11; i++) {
Integer take = lbq.take();
System.out.println(take+"被取出。");
}
}
}
3.4.3 DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
DelayQueue只能添加(offer/put/add)实现了Delayed接口的对象,意思是说我们不能想往DelayQueue里添加什么就添加什么,不能添加int、也不能添加String进去,必须添加我们自己的实现了Delayed接口的类的对象
使用场景:
DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
3.4.3.1 案例
/*
添加到延时队列中的元素必须实现Delayed接口
*/
class MyDelayedTask implements Delayed {
private String name ;
private long start = System.currentTimeMillis();
private long time ;
public MyDelayedTask(String name,long time) {
this.name = name;
this.time = time;
}
/**
* 需要实现的接口,获得延迟时间 用过期时间-当前时间
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert((start+time) - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
/**
* 用于延迟队列内部比较排序 当前时间的延迟时间 - 比较对象的延迟时间
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
MyDelayedTask o1 = (MyDelayedTask) o;
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
return "MyDelayedTask{" +
"name='" + name + '\'' +
", time=" + time +
'}';
}
}
/*
@Author xiangge
@Date 2023/8/10
@Description 延时队列
说明: 从队列中取出数据时会基于延时时间取出
特点:1. 无界队列
2. 添加的元素对象的模板必须实现Delayed接口
*/
public class DelayBlockingQueueDemo1 {
public static void main(String[] args) throws InterruptedException {
final DelayQueue delayQueue = new DelayQueue();
new Thread(new Runnable() {
@Override
public void run() {
//向队列里面放不同延时时间的任务
delayQueue.offer(new MyDelayedTask("task1",10000));
delayQueue.offer(new MyDelayedTask("task2",3900));
delayQueue.offer(new MyDelayedTask("task3",1900));
delayQueue.offer(new MyDelayedTask("task4",5900));
delayQueue.offer(new MyDelayedTask("task5",6900));
delayQueue.offer(new MyDelayedTask("task6",7900));
delayQueue.offer(new MyDelayedTask("task7",4900));
}
}).start();
//使用死循环去取,take会阻塞
while (true) {
Delayed take = delayQueue.take();
System.out.println(take);
}
}
}
3.4.4 PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
3.4.4.1 案例
public class Person implements Comparable<Person>{
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(int id, String name) {
super();
this.id = id;
this.name = name;
}
public Person() {
}
@Override
public String toString() {
return this.id + ":" + this.name;
}
// 按照id属性升序排序
@Override
public int compareTo(Person person) {
return this.id > person.getId() ? 1 : ( this.id < person.getId() ? -1 :0);
}
}
public class TestPriorityBlockingQueue {
public static void main(String[] args) {
PriorityBlockingQueue<Person> pbq = new PriorityBlockingQueue<>();
pbq.add(new Person(3,"person3"));
pbq.add(new Person(2,"person2"));
pbq.add(new Person(1,"person1"));
pbq.add(new Person(4,"person4"));
// 开启一个消费者线程:到pbq中按照id从小到大的顺序,消费数据 此时的id相当于优先级
new Thread(new Runnable() {
@Override
public void run() {
while (true){
Person take = null;
try {
take = pbq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(take+"被取出.");
}
}
}).start();
}
}
运行结果如下图:
对结果分析,每次添加一个元素,PriorityBlockingQueue中的person都会执行compareTo方法进行排序,但是只是把第一个元素排在首位,其他元素按照队列的一系列复杂算法排序。这就保障了每次获取到的元素都是经过排序的第一个元素。
3.4.5 SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给哪些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
原理图:
3.4.5.1 案例
/*
无缓存阻塞队列:SynchronousQueue
*/
public class TestSynchronousQueue {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开始放");
try {
Thread.sleep(2000);
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("放完了");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开始取");
try {
System.out.println("取出来的结果为: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("取完了");
}
});
putThread.start();
takeThread.start();
}
}
运行结果如下图:
3.5 小结
BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待与唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。