队列在Java中的应用
Java的线程池直接使用了队列的API,锁借鉴了队列的思想,重新实现了队列。所以队列在这两个的实现上都发挥了关键作用。
1. 队列和线程池
队列在线程池中的作用—存放任务请求
使用下面的代码可以创建固定线程数目的线程池,
ExecutorService executorService = Executors.newFixedThreadPool(10);
// submit提交任务
executorService.submit(() -> System.out.println(Thread.currentThread().getName() + " is run"));
// 打印结果
pool-1-thread-1 is run
上面的代码中创建了一个线程数目为10的线程池,并提交了一个任务给线程池去做。该任务被线程池中的某个线程获取并执行。
实际工作中,任务量的大小是无法控制的,假设还是使用上面创建的10个线程,此时来了100个任务,10个线程都在运行的时候,还会剩余90个任务。这90个任务就会被放入一个队列中,在队列中排队等待线程池中的线程将其执行。
队列在线程池中的地位很关键,用于存放待执行的任务。
线程池中的队列类型
1)LinkedBlockingQueue
上面使用的newFixedTheadPool是固定大小的线程池,线程池初始化后,其中的线程数目无法改变。该类构造函数源码如下,
// ThreadPoolExecutor 初始化时,第一个参数表示 coreSize,第二个参数是 maxSize,coreSize == maxSize,
// 表示线程池初始化时,线程大小已固定,所以叫做固定(Fixed)线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
LinkedBlockingQueue的最大容量是Integer的最大值。实际工作中,常常不建议直接使用newFixedThreadPool,因为底层队列容量过大,当队列中等待线程过多时,无法满足实时响应的请求。
举例而言,
- 线程池中有10个线程工作
- 100个请求过来,假设每个任务的默认超时时间为2秒,而10个线程处理完100个请求需要3秒
- 有一部分线程等待两秒后,返回报错信息,但其任务依旧在队列中等待
- 10个线程完成这部分报错请求的任务,但是无法将执行结果返还给调用方
- 调用方明明收到了报错信息,更新状态后发现之前的请求已经被执行完成
上图可以用来展示上面步骤中提及的情况,这种现象在实际开发过程中属于事故,应该避免。
另一个相似的类,newSingleFixedThreadExecutor底层依旧是LinkedBlockingQueue,只是规定线程池中仅存在一个线程,
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
// 前两个参数规定了这个线程池一次只能消费一个线程
// 第五个参数使用的是 LinkedBlockingQueue,说明当请求超过单线程消费能力时,就会排队
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SynchronousQueue
newCashedThreadPool类的底层是SychronousQueue队列,
public static ExecutorService newCachedThreadPool() {
// 第五个参数是 SynchronousQueue
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
该队列没有大小的概念,无论有多少请求,该队列都能扛住,这是该队列的优点。但是缺点在于无法立刻返回结果,像队列中put数据时,需要等待有线程take数据后,才能正常返回。
当请求量很大但是消费能力较差时,会有大量的请求被阻塞,必须等待慢慢被释放,所以使用也同样需要谨慎。
DelayedWorkQueue
newScheduledThreadPool和newSingleScheduledThreadPool是定时任务线程池,
新的延迟请求先入队,延迟时间到了线程池就能从队列中取出元素执行请求。
2. 队列和锁
队列在锁中的作用—管理争锁失败的线程
锁的代码一般书写方式如下,
ReentrantLock lock = new ReentrantLock();
try{
lock.lock();
// do something
}catch(Exception e){
//throw Exception;
}finally {
lock.unlock();
}
同一个时刻,只有一个线程才能拿到锁,其余线程都会到锁的同步队列中等待,
队列在锁中的作用是管理未获取到锁的线程。