四种线程池的使用(JAVA笔记-线程基础篇)
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。——百度百科
- 简单来说,线程池(thread pool)就像池子一样,不过池子里面可能放的是水,需要水就去池子里打水。而线程池里面放的是线程,需要线程的时候就去线程池里取线程。因为一个线程可能只执行一个很小的功能,比如计算个1+1等于几,如果每次都重新创建一个新线程计算,用完就把线程给销毁了,这样很浪费效率(创建和销毁线程很费时费力)。
- 如果我创建一个线程去计算1+1,计算完把这条线程放到线程池里面,让线程池帮我维护,别让程序给销毁了。下次计算1+2的时候,直接跟线程池要线程就好,不用再创建了。
JAVA提供的线程池工具类
在JAVA的java.util.concurrent
包(大佬们都叫这个包JUC)下面有一个工具类Executors
。这个类封装了创建四种线程池的方法。底层都是通过new ThreadPoolExecutor(...)
来实现ExecutorService
对象的。
-
newSingleThreadExecutor()单线程池
- 作用:创建一个只有一个线程的线程池,如果有超过一个的任务进来,就放在队列中等待。等上一个任务执行完再来执行下一个。
- 适用范围:适合长期执行的任务,或者需要按照顺序执行的一系列任务。
创建线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
为了使用这个线程池,我们创建3个
Thread
对象,也就是线程需要执行的任务。Thread thread1=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!"); } });
使用刚才创建的线程池来执行
thread
对象。singleThreadExecutor.execute(thread1); singleThreadExecutor.execute(thread2); singleThreadExecutor.execute(thread3);
完整代码
public class MyTest { public static void main(String[] args) { ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); Thread thread1=new Thread(new Runnable() { public void run() { //使用getName()获取执行这段代码的线程名。下同 System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); singleThreadExecutor.execute(thread1); singleThreadExecutor.execute(thread2); singleThreadExecutor.execute(thread3); } }
输出:
pool-1-thread-1 使用线程池中的线程执行的!
pool-1-thread-1 使用线程池中的线程执行的!
pool-1-thread-1 使用线程池中的线程执行的!看!上面输出的结果很好玩,线程名称都是pool-1-thread-1
如果我们不用singleThreadExecutor.execute(thread1);
而直接使用thread1.start();
会怎么样呢?
直接改代码public class MyTest { public static void main(String[] args) { Thread thread1=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!"); } }); //这里跟上面的例子不一样 thread1.start(); thread2.start(); thread3.start(); } }
输出结果:
Thread-0 使用线程池中的线程执行的!
Thread-2 使用线程池中的线程执行的!
Thread-1 使用线程池中的线程执行的!这次运行三段代码的线程名不同了,也就是说是三个不同的线程。
而且更有意思的是,如果多运行几遍,这三个线程输出顺序也可能会改变。这就说明他们三个线程被CPU“翻牌”的概率是不一定的,并不是哪个线程先.start()
哪个线程里的代码先执行完。有时候Thread-0更受宠一点,而有时候Thread-2更受宠一点。结论:
- newSingleThreadExecutor()创建的线程池里面只有一个可用线程。
- 任务的执行是按照添加的顺序执行的。
- 如果线程正在执行。这时候有其他的任务进来,需要线程执行,就会把其放在队列中等待。队列是五界的。
队列无界,通俗的说就是来者不拒,不管执不执行,你既然任务来了,就放在队列里等着吧。等这一个线程挨个的执行。
细想这个线程池是不是又问题。来者不拒?我给你一百万、一千万、一亿个任务呢??你内存不是爆表了嘛?就OOM拉。
-
newFixedThreadPool()
- 作用:创建一个有固定大小的线程池,也就是指定线程池里面的线程数量。
- 适合场景:适合长期执行的任务,并且希望控制线程数量。
创建一个有两个线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
写三个
thread
对象来测试一下public class MyTest { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2); Thread thread1=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread1"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread2"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 使用线程池中的线程执行的!thread3"); } }); fixedThreadPool.execute(thread1); fixedThreadPool.execute(thread2); fixedThreadPool.execute(thread3); } }
输出:
pool-1-thread-1 使用线程池中的线程执行的!thread1
pool-1-thread-2 使用线程池中的线程执行的!thread2
pool-1-thread-1 使用线程池中的线程执行的!thread3可以看出
pool-1-thread-1
执行了thread1
和thread3
的代码,而pool-1-thread-2
执行了thread2
的代码。这也说明线程池里至少有两个线程。结论:
- newFixedThreadPool()创建一个拥有固定线程数量的线程池,线程数量通过传参设置。如:
Executors.newFixedThreadPool(2)
。
-
newCachedThreadPool()缓存型线程池
- 作用:创建一个缓存型的线程池,有新任务进来,先查找有没有空闲的线程,如果有拿过来用,如果没有,就会创建新的线程并放到核心线程池里。
此线程池里的线程存活是有一定时间的,如果一个线程空闲了一定时间没有被使用,就会被销毁。
所以这个线程池里的线程不会太多空闲,也不会有不足。 - 使用场景:适合执行周期短而多的任务。
创建一个缓存型线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
用法上同。
- 作用:创建一个缓存型的线程池,有新任务进来,先查找有没有空闲的线程,如果有拿过来用,如果没有,就会创建新的线程并放到核心线程池里。
-
newScheduledThreadPool()计划型线程池
- 作用:创建一个固定大小的线程池,线程池内的线程存活周期无限长,支持定时或者周期性的执行某个任务(比如隔3秒执行一次)等。
- 使用场景:有周期性或者定时执行某个任务的需要。
声明一个有三个线程的计划型线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
创建三个任务,也就是
Thread
对象。Thread thread1=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread1"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread2"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread3"); } });
十秒后执行任务1,
thread1
scheduledExecutorService.schedule(thread1,10,TimeUnit.SECONDS);
说明:
第一个参数是任务,也就是thread实例对象
第二个参数是一个数字,跟第三个参数组合使用来确定多长时间后执行。
第三个参数是一个TimeUnit
的枚举类型。TimeUnit.SECONDS
是秒、TimeUnit.HOURS
是小时等等。10秒后开始,每个3秒执行一遍任务2
scheduledExecutorService.scheduleAtFixedRate(thread2,10,3,TimeUnit.SECONDS);
比上面的多了一参数,就是第三个(上面例子中的3),这个参数代表每隔多长时间执行一次任务。
完整测试代码:
public class MyTest { public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); Thread thread1=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread1"); } }); Thread thread2=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread2"); } }); Thread thread3=new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"使用线程池中的线程执行的!thread3"); } }); //10秒后执行任务1 scheduledExecutorService.schedule(thread1,10,TimeUnit.HOURS); //10秒后开始,每个3秒执行一遍任务2 scheduledExecutorService.scheduleAtFixedRate(thread2,10,3,TimeUnit.SECONDS); //没有任何特点的执行任务3 scheduledExecutorService.execute(thread3); } }
结尾
很多大佬都不推荐直接使用Executors
封装好的实现线程的方法,因为在大型项目中会暴露出很多问题。比如我们常用的newFixedThreadPool()
它的储存多余任务的队列是无边界的。
Executors
中的源码是这么实现的:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
先不用管其他的参数,看看第五个参数new LinkedBlockingQueue<Runnable>()
,它是一个无界的队列。所以就可以无限制的往里面增加等待任务。这样添加个几千万几百万的不久内存溢出了嘛。
其他的也多多少少存在一些问题。所以大佬们推荐自己动手创建自定义的线程池。
如果有缘,我们下一篇笔记见。
作者:BobC
文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。