文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

写给小白看的线程池,学会了吗?

2024-12-03 12:07

关注

为什么要用线程池呢?

下面是一段创建线程并运行的代码:

  1. for (int i = 0; i < 100; i++) { 
  2.     new Thread(() -> { 
  3.         System.out.println("run thread->" + Thread.currentThread().getName()); 
  4.         userService.updateUser(....); 
  5.     }).start(); 

我们想使用这种方式去做异步,或者提高性能,然后将某些耗时操作放入一个新线程去运行。

这种思路是没问题的,但是这段代码是存在问题的,有哪些问题呢?下面我们就来看看有哪些问题;

既然我们上面使用手动创建线程会存在问题,那有解决方法吗?

答案:有的,使用线程池。

线程池介绍

线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。

线程池有什么优点?

线程池使用

在JDK中rt.jar包下JUC(java.util.concurrent)创建线程池有两种方式:ThreadPoolExecutor 和 Executors,其中 Executors又可以创建 6 种不同的线程池类型。

ThreadPoolExecutor 的使用

线程池使用代码如下:

  1. import java.util.concurrent.LinkedBlockingQueue; 
  2. import java.util.concurrent.ThreadPoolExecutor; 
  3. import java.util.concurrent.TimeUnit; 
  4.  
  5. public class ThreadPoolDemo { 
  6.     private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100)); 
  7.  
  8.     public static void main(String[] args) { 
  9.         threadPoolExecutor.execute(new Runnable() { 
  10.             @Override 
  11.             public void run() { 
  12.                 System.out.println("田先生您好"); 
  13.             } 
  14.         }); 
  15.     } 

以上程序执行结果如下:

田先生您好

核心参数说明

ThreadPoolExecutor的构造方法有以下四个:

可以看到最后那个构造方法有 7 个构造参数,其实前面的三个构造方法只是对最后那个方法进行包装,并且前面三个构造方法最终都是调用最后那个构造方法,所以我们这里就来聊聊最后那个构造方法。

参数解释

corePoolSize

线程池中的核心线程数,默认情况下核心线程一直存活在线程池中,如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,如果线程池一直闲置并超过了 keepAliveTime 所指定的时间,核心线程就会被终止。

maximumPoolSize

最大线程数,当线程不够时能够创建的最大线程数。

keepAliveTime

线程池的闲置超时时间,默认情况下对非核心线程生效,如果闲置时间超过这个时间,非核心线程就会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,核心线程如果超过闲置时长也会被回收。

unit

配合 keepAliveTime 使用,用来标识 keepAliveTime 的时间单位。

workQueue

线程池中的任务队列,使用 execute() 或 submit() 方法提交的任务都会存储在此队列中。

threadFactory

为线程池提供创建新线程的线程工厂。

rejectedExecutionHandler

线程池任务队列超过最大值之后的拒绝策略,RejectedExecutionHandler 是一个接口,里面只有一个 rejectedExecution 方法,可在此方法内添加任务超出最大值的事件处理。ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:

包含所有参数的使用案例:

  1. public class ThreadPoolExecutorTest { 
  2.     public static void main(String[] args) throws InterruptedException, ExecutionException { 
  3.         ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 
  4.                 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(2), 
  5.                 new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); 
  6.         threadPool.allowCoreThreadTimeOut(true); 
  7.         for (int i = 0; i < 10; i++) { 
  8.             threadPool.execute(new Runnable() { 
  9.                 @Override 
  10.                 public void run() { 
  11.                     System.out.println(Thread.currentThread().getName()); 
  12.                     try { 
  13.                         Thread.sleep(2000); 
  14.                     } catch (InterruptedException e) { 
  15.                         e.printStackTrace(); 
  16.                     } 
  17.                 } 
  18.             }); 
  19.         } 
  20.     } 
  21. class MyThreadFactory implements ThreadFactory { 
  22.     private AtomicInteger count = new AtomicInteger(0); 
  23.     @Override 
  24.     public Thread newThread(Runnable r) { 
  25.         Thread t = new Thread(r); 
  26.         String threadName = "MyThread" + count.addAndGet(1); 
  27.         t.setName(threadName); 
  28.         return t; 
  29.     } 

运行输出:

  1. main 
  2. MyThread1 
  3. main 
  4. MyThread1 
  5. MyThread1 
  6. .... 

这里仅仅是为了演示所有参数自定义,并没有其他用途。

execute() 和 submit()的使用

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 方法可以接收线程池执行的返回值。

下面分别来看两个方法的具体使用和区别:

  1. // 创建线程池 
  2. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100)); 
  3. // execute 使用 
  4. threadPoolExecutor.execute(new Runnable() { 
  5.     @Override 
  6.     public void run() { 
  7.         System.out.println("老田您好"); 
  8.     } 
  9. }); 
  10. // submit 使用 
  11. Future future = threadPoolExecutor.submit(new Callable() { 
  12.     @Override 
  13.     public String call() throws Exception { 
  14.         System.out.println("田先生您好"); 
  15.         return "返回值"
  16.     } 
  17. }); 
  18. System.out.println(future.get()); 

以上程序执行结果如下:

  1. 老田您好 
  2. 田先生您好 
  3. 返回值 

Executors

Executors 执行器创建线程池很多基本上都是在 ThreadPoolExecutor 构造方法上进行简单的封装,特殊场景根据需要自行创建。可以把Executors理解成一个工厂类 。Executors可以创建 6 种不同的线程池类型。

下面对这六个方法进行简要的说明:

newFixedThreadPool

创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。

newCacheThreadPool

短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。如果现有线程没有可用的,则创建一个新线程并添加到池中,如果有被使用完但是还没销毁的线程,就复用该线程。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

newScheduledThreadPool

创建一个数量固定的线程池,支持执行定时性或周期性任务。

newWorkStealingPool

Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器CPU 处理器数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newSingleThreadScheduledExecutor

此线程池就是单线程的 newScheduledThreadPool。

线程池如何关闭?

线程池关闭,可以使用 shutdown() 或 shutdownNow() 方法,它们的区别是:

下面用代码来模拟 shutdown() 之后,给线程池添加任务,代码如下:

  1. import java.util.concurrent.*; 
  2. import java.util.concurrent.atomic.AtomicInteger; 
  3.  
  4. public class ThreadPoolExecutorAllArgsTest { 
  5.    public static void main(String[] args) throws InterruptedException, ExecutionException { 
  6.        //创建线程池 
  7.        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 
  8.                10L, TimeUnit.SECONDS, new LinkedBlockingQueue(2), 
  9.                new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); 
  10.        threadPoolExecutor.allowCoreThreadTimeOut(true); 
  11.        //提交任务 
  12.        threadPoolExecutor.execute(() -> { 
  13.            for (int i = 0; i < 3; i++) { 
  14.                System.out.println("提交任务" + i); 
  15.                try { 
  16.                    Thread.sleep(3000); 
  17.                } catch (InterruptedException e) { 
  18.                    System.out.println(e.getMessage()); 
  19.                } 
  20.            } 
  21.        }); 
  22.        threadPoolExecutor.shutdown(); 
  23.        //再次提及任务 
  24.        threadPoolExecutor.execute(() -> { 
  25.            System.out.println("我想再次提及任务"); 
  26.        }); 
  27.    } 

以上程序执行结果如下:

  1. 提交任务0 
  2. 提交任务1 
  3. 提交任务2 

可以看出,shutdown() 之后就不会再接受新的任务了,不过之前的任务会被执行完成。

面试题

面试题1:ThreadPoolExecutor 有哪些常用的方法?

这些方法可以用来终止线程池、线程池监控等。

面试题2:说说submit(和 execute两个方法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

说说线程池创建需要的那几个核心参数的含义

ThreadPoolExecutor 最多包含以下七个参数:

面试题3:shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

面试题6:了解过线程池的工作原理吗?

当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

面试题5:线程池中核心线程数量大小怎么设置?

「CPU密集型任务」:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

「IO密集型任务」:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

面试题7:线程池为什么需要使用(阻塞)队列?

主要有三点:

面试题8:线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下

  1. while (task != null || (task = getTask()) != null) {})。 

不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

面试题9:了解线程池状态吗?

通过获取线程池状态,可以判断线程池是否是运行状态、可否添加新的任务以及优雅地关闭线程池等。

RUNNING:线程池的初始化状态,可以添加待执行的任务。

SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。

STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。

TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。

TERMINATED:线程池终止状态。

面试题10:知道线程池中线程复用原理吗?

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

总结

本文通过没有使用线程池带来的弊端,Executors介绍,Executors的六种方法介绍、如何使用线程池,了解线程池原理,核心参数,以及10到线程池面试题。

本文转载自微信公众号「Java后端技术全栈」,可以通过以下二维码关注。转载本文请联系Java后端技术全栈公众号。

 

来源:Java后端技术全栈内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯