深⼊理解Java⾃带的线程池和缓冲队列
前⾔
线程池是什么
线程池的概念是初始化线程池时在池中创建空闲的线程,⼀但有⼯作任务,可直接使⽤线程池中的线程进⾏执⾏⼯作任务,任务执⾏完成后⼜返回线程池中成为空闲线程。使⽤线程池可以减少线程的创建和销毁,提⾼性能。
举个例⼦:我是⼀个包⼯头,代表线程池,⼿底下有若⼲⼯⼈代表线程池中的线程。如果我没接到项⽬,那么⼯⼈就相当于线程池中的空闲线程,⼀但我接到了项⽬,我可以⽴刻让我⼿下的⼯⼈去⼯作,每个⼯⼈同⼀时间执⾏只执⾏⼀个⼯作任务,执⾏完了就去
执⾏另⼀个⼯作任务,知道没有⼯作任务了,这时⼯⼈就可以休息了(原谅我让⼯⼈⽆休⽌的⼯作),也就是⼜变成了线程池中的空闲线程池。
队列是什么
队列作为⼀个缓冲的⼯具,当没有⾜够的线程去处理任务时,可以将任务放进队列中,以队列先进先出的特性来执⾏⼯作任务
举个例⼦,我⼜是⼀个包⼯头,⼀开始我只接了⼀个⼩项⽬,所以只有三个⼯作任务,但我⼿底下有四个⼯⼈,那么其中三⼈各领⼀个⼯作任务去执⾏就好了,剩下⼀个⼈就先休息。但突然我⼜接到了⼏个⼤项⽬,那么有现在有很多⼯作任务了,但⼿底下的⼯⼈不够啊。
那么我有两个选择:
(1)雇佣更多的⼯⼈
(2)把⼯作任务记录下来,按先来后到的顺序执⾏
但雇佣更多等⼯⼈需要成本啊,对应到计算机就是资源的不⾜,所以我只能把⼯作任务先记录下来,这样就成了⼀个队列了。
为什么要使⽤线程池
假设我⼜是⼀个包⼯头,我现在⼿底下没有⼯⼈了,但我接到了⼀个项⽬,有了⼯作任务要执⾏,那我肯定要去⼯⼈了,但招⼈成本是很⾼的,⼯作完成后还要给遣散费,这样算起来好像不值,所以我事先雇佣了固定的⼏个⼯⼈作为我的长期员⼯,有⼯作任务就⼲活,没有就休息,如果⼯作任务实在太
多,那我也可以再临时雇佣⼏个⼯⼈。⼀来⼆去⼯作效率⾼了,付出的成本也低了。Java⾃带的线程池的原理也是如此。
Java⾃带的线程池
Executor接⼝是Executor的⽗接⼝,基于⽣产者--消费者模式,提交任务的操作相当于⽣产者,执⾏任务的线程则相当于消费者,如果要在程序中实现⼀个⽣产者--消费者的设计,那么最简单的⽅式通常是使⽤Executor。
ExecutorService接⼝是对Executor接⼝的扩展,提供了对⽣命周期的⽀持,以及统计信息收集、应⽤程序管理机制和性能监视等机制。
常⽤的使⽤⽅法是调⽤Executors中的静态⽅法来创建⼀个连接池。
(1)newFixedThreadPool
代码演⽰:
1public class ThreadPoolTest {
2public static void main(String[] args){
3        ExecutorService executor = wFixedThreadPool(3);
4for (int i = 0; i < 4; i++){
5            Runnable runnable = new Runnable() {
6public void run() {
7                    CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,⽤于阻塞线程
8                    System.out.println(Thread.currentThread().getName() + "正在执⾏");
9try {
10                        countDownLatch.await();
11                    } catch (InterruptedException e) {
12                        e.printStackTrace();
13                    }
14                }
15            };
16            ute(runnable);
17        }
18    }
19 }
测试结果:
pool-1-thread-1正在执⾏
pool-1-thread-3正在执⾏
pool-1-thread-2正在执⾏
newFixedThreadPool将创建⼀个固定长度的线程池,每当提交⼀个任务时就会创建⼀个线程,直到达
线程池的最⼤数量,这时线程池的规模不再变化(如果某个线程由于发⽣了未预期的Exception⽽结束,那么线程池会补充⼀个新的线程)。上述代码中最⼤的线程数是3,但我提交了4个任务,⽽且每个任务都阻塞住,所以前三个任务占⽤了线程池所有的线程,那么第四个任务永远也不会执⾏,因此该线程池配套使⽤的队列也是⽆界的。所以在使⽤该⽅法创建线程池时要根据实际情况看需要执⾏的任务是否占⽤过多时间,会不会影响后⾯任务的执⾏。
(2)newCachedThreadPool
测试代码:
1public class ThreadPoolTest {
2public static void main(String[] args){
3        ExecutorService executor = wCachedThreadPool();
4for (int i = 0; i < 4; i++){
5            Runnable runnable = new Runnable() {
6public void run() {
7                    CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,⽤于阻塞线程
8                    System.out.println(Thread.currentThread().getName() + "正在执⾏");
9try {
10                        countDownLatch.await();
11                    } catch (InterruptedException e) {
12                        e.printStackTrace();
13                    }
14                }
15            };
16            ute(runnable);
17        }
18    }
19 }
测试结果:
pool-1-thread-1正在执⾏
pool-1-thread-3正在执⾏
pool-1-thread-2正在执⾏
pool-1-thread-4正在执⾏
newCachedThreadPool将创建⼀个可缓存的线程池。如果线程池的当前规模超过了处理需求时,那么就会回收部分空闲的线程(根据空闲时间来回收),当需求增加时,此线程池⼜可以智能的添加新线程来处理任务。此线程池不会对线程池⼤⼩做限制,线程池⼤⼩完全依赖于操作系统(或者说JVM)能够创建的最⼤线程⼤⼩。
(3)newSingleThreadExecutor
测试代码:
1public class ThreadPoolTest {
2public static void main(String[] args){
3        ExecutorService executor = wSingleThreadExecutor();
4for (int i = 0; i < 4; i++){
5final int index = i;
6            Runnable runnable = new Runnable() {
7public void run() {
8                    System.out.println(Thread.currentThread().getName() + "正在执⾏⼯作任务--- >" + index);
9                }
10            };
11            ute(runnable);java线程池创建的四种
12        }
13    }
14 }
测试结果:
pool-1-thread-1正在执⾏⼯作任务--- >0
pool-1-thread-1正在执⾏⼯作任务--- >1
pool-1-thread-1正在执⾏⼯作任务--- >2
pool-1-thread-1正在执⾏⼯作任务--- >3
newSingleThreadExecutor是⼀个单线程的Executor,它创建单个⼯作者线程来执⾏任务,如果这个线程异常结束,会创建另⼀个线程来代替。newSingleThreadExecutor能确保依照任务在队列中的顺序来串⾏执⾏。
(4)newScheduledThreadPool
测试代码:
1public class ThreadPoolTest {
2public static void main(String[] args){
3        ScheduledExecutorService executor = wScheduledThreadPool(3);
4for (int i = 0; i < 3; i++){
5final int index = i;
6            Runnable runnable = new Runnable() {
7public void run() {
8                    System.out.println(Thread.currentThread().getName() + "延时1s后,每5s执⾏⼀次⼯作任务--- >" + index);
9                }
10            };
11            executor.scheduleAtFixedRate(runnable,1,5,TimeUnit.SECONDS);
12        }
13    }
14 }
测试结果:
pool-1-thread-1延时1s后,每5s执⾏⼀次⼯作任务--- >0
pool-1-thread-2延时1s后,每5s执⾏⼀次⼯作任务--- >1
pool-1-thread-3延时1s后,每5s执⾏⼀次⼯作任务--- >2
pool-1-thread-1延时1s后,每5s执⾏⼀次⼯作任务--- >0
pool-1-thread-3延时1s后,每5s执⾏⼀次⼯作任务--- >2
pool-1-thread-2延时1s后,每5s执⾏⼀次⼯作任务--- >1
newScheduledThreadPool创建了⼀个固定长度的线程池,⽽且以延迟或定时或周期的⽅式来执⾏任务,类似于Timer。可应⽤于重发机制。
以上四种创建线程池的⽅法其实都是调⽤以下这个⽅法,只是参数不⼀样
corePoolSize  ---------------------> 核⼼线程数
maximumPoolSize ---------------> 最⼤线程数
keepAliveTime --------------------> 当线程数⼤于核⼼时,此为终⽌前多余的空闲线程等待新任务的最长时间
unit -----------------------------------> 时间单位
workQueue ------------------------> ⽤于存储⼯作⼯⼈的队列
threadFactory ---------------------> 创建线程的⼯⼚
handler ------------------------------> 由于超出线程范围和队列容量⽽使执⾏被阻塞时所使⽤的处理程序
常⽤的⼏种队列
(1)ArrayBlockingQueue:规定⼤⼩的BlockingQueue,其构造必须指定⼤⼩。其所含的对象是FIFO顺序排序的。
(2)LinkedBlockingQueue:⼤⼩不固定的BlockingQueue,若其构造时指定⼤⼩,⽣成的BlockingQueue有⼤⼩限制,不指定⼤⼩,其⼤⼩有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
(3)PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,⽽是依据对象的⾃然顺序或者构造函数的Comparator决定。
(4)SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。
排队策略(以下排队策略⽂字来⾃------->www.oschina/question/565065_86540)
排队有三种通⽤策略:
直接提交。⼯作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程⽽不保持它们。在此,如果不存在可⽤于⽴即运⾏任务的线程,则试图把任务加⼊队列将失败,因此会构造⼀个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求⽆界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许⽆界线程具有增长的可能性。
⽆界队列。使⽤⽆界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就⽆效了。)当每个任务完全独⽴于其他任务,即任务执⾏互不影响时,适合于使⽤⽆界队列;例如,在 Web页服务器中。这种排队可⽤于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许⽆界线程具有增长的可能性。
有界队列。当使⽤有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防⽌资源耗尽,但是可能较难调整和控制。队列⼤⼩和最⼤池⼤⼩可能需要相互折衷:使⽤⼤型队列和⼩型池可以最⼤限度地降低 CPU 使⽤率、操作系统资源和上下⽂切换开销,但是可能导致⼈⼯降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使⽤⼩型
队列通常要求较⼤的池⼤⼩,CPU使⽤率较⾼,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
我们选其中的LinkedBlockingQueue队列来解析
在上述Java⾃带的创建线程池的⽅法中,newFixedThreadPool使⽤的队列就是LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
如果需要执⾏的⼯作任务少于核⼼线程数,那么直接使⽤线程池的空闲线程执⾏任务,如果任务不断增加,超过核⼼线程数,那么任务将被放进队列中,⽽且是没有限制的,线程池中的线程也不会增加。
其他线程池的⼯作队列也是根据排队的通⽤策略来进⾏⼯作,看客们可以⾃⼰分析。
总结:
没有创建连接池的⽅式,只有最适合的⽅式,使⽤Java⾃带的⽅式创建或者⾃⼰创建连接池都是可⾏的,但都要依照⾃⾝的业务情况选择合适的⽅式。
如果你的⼯作任务的数量在不同时间差距很⼤,那么如果使⽤newFixedThreadPool创建固定的线程就不合适,创建少了到时队列⾥会塞进太多的⼯作任务导致处理不及时,创建多了会导致⼯作任务少时有太多的线程处于空闲状态造成资源浪费。
所以还是需要根据实际情况使⽤适合的创建⽅式。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。