在执行一系列带有IO操作(例如下载文件),且互不相关的异步任务时,采用多线程可以很极大的提高运行效率。线程池包含了一系列的线程,并且可以管理这些线程。例如:创建线程,销毁线程等。本文将介绍如何使用Java中的线程池执行任务。

1 任务类型

在使用线程池执行任务之前,我们弄清楚什么任务可以被线程池调用。按照任务是否有返回值可以将任务分为两种,分别是实现Runnable的任务类(无参数无返回值)和实现Callable接口的任务类(无参数有返回值)。在打代码时根据需求选择对应的任务类型。

1.1 实现Runnable接口的类

多线程任务类型,首先自然想到的就是实现 Runnable 接口的类,Runnable接口提供了一个抽象方法run,这个方法无参数,无返回值。例如:

Runnable task = newRunnable() {

@Overridepublic voidrun() {

System.out.println("Execute task.");

}

};

或者Java 8 及以上版本更简单的写法:

Runnable task = ()->{

System.out.println("Execute task.");

};

1.2 实现Callable接口的类

于Runnable一样Callable也只有一个抽象方法,不过该抽象方法有返回值。在实现该接口的时候需要制定返回值的类型。例如:

Callable callableTask = ()-> "finished";

2 线程池类型

java.util.concurrent.Executors 提供了一系列静态方法来创建各种线程池。下面例举出了主要的一些线程池及特性,其它未例举线程池的特性可由下面这些推导出来。

2.1 线程数固定的线程池 Fixed Thread Pool

顾名思义,这种类型线程池线程数量是固定的。如果线程数量设置为n,则任何时刻该线程池最多只有n个线程处于运行状态。当线程池中处于饱和运行状态时,再往线程池中提交的任务会被放到执行队列中。如果线程池处于不饱和状态,线程池也会一直存在,直到ExecuteService 的shutdown方法被调用,线程池才会被清除。

//创建线程数量为5的线程池。

ExecutorService executorService = Executors.newFixedThreadPool(5);

2.2 可缓存的线程池 Cached Thread Pool

这种类型的线程池初始大小为0个线程,随着往池里不断提交任务,如果线程池里面没有闲置线程(0个线程也表示没有闲置线程),则会创建新的线程,保证没有任务在等待;如果有闲置线程,则复用闲置状态线程执行任务。处于闲置状态的线程只会在线程池中缓存60秒,闲置时间达到60s的线程会被关闭并移出线程池。在处理大量短暂的(官方说法:short-lived)异步任务时可以显著得提供程序性能。

//创建一个可缓存的线程池

ExecutorService executorService = Executors.newCachedThreadPool();

2.3 单线程池

这或许不能叫线程池了,由于它里面的线程永远只有1个,而且自始至终都只有1个(为什么说这句话,因为要和 Executors.newFixedThreadPool(1) 区别开来),所以还是叫它“单线程池把”。你尽可以往单线程池中添加任务,但是每次只执行1个,且任务是按顺序执行的。如果前面的任务出现了异常,当前线程会被销毁,但1个新的线程会被创建用来执行后面的任务。以上这些和线程数只有1个的线程Fixed Thread Pool一样。两者唯一不同的是, Executors.newFixedThreadPool(1)可以在运行时修改它里面的线程数,而 Executors.newSingleThreadExecutor() 永远只能有1个线程。至于“为什么”,我准备专门再写一篇博客通过源代码来分析。

//创建一个单线程池

ExecutorService executorService = Executors.newSingleThreadExecutor();

2.4 工作窃取线程池

扒开源码,会发现工作窃取线程池本质是 ForkJoinPool ,这类线程池充分利用CPU多核处理任务,适合处理消耗CPU资源多的任务。它的线程数不固定,维护的任务队列有多个,当一个任务队列完成时,相应的线程会从其它的任务队列中窃取任务执行,这也意味着任务的开始执行顺序并和提交顺序相同。如果有更高的需求,可以直接通过ForkJoinPool获取线程池。

//创建一个工作窃取线程池,使用CPU核数等于机器的CPU核数

ExecutorService executorService =Executors.newWorkStealingPool();//创建一个工作窃取线程池,使用CPU 3 个核进行计算,工作窃取线程池不能设置线程数

ExecutorService executorService2 = Executors.newWorkStealingPool(3);

2.5 计划任务线程池

计划任务线程池可以按计划执行某些任务,例如:周期性的执行某项任务。

//获取一个大小为2的计划任务线程池

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);//添加一个打印当前线程信息计划任务,该任务在3秒后执行

scheduledExecutorService.schedule(() -> { System.out.println(Thread.currentThread()); }, 3, TimeUnit.SECONDS);//添加一个打印当前线程信息计划任务,该任务在2秒后首次执行,之后每5秒执行一次。如果任务执行时间超过了5秒,则下一次将会在前一次执行完成之后立即执行

scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println(Thread.currentThread()); }, 2, 5, TimeUnit.SECONDS);//添加一个打印当前线程信息计划任务,该任务在2秒后首次执行,之后每次在任务执行之后5秒执行下一次。

scheduledExecutorService.scheduleWithFixedDelay(() -> { System.out.println(Thread.currentThread()); }, 2, 5, TimeUnit.SECONDS);//逐个清除 idle 状态的线程

scheduledExecutorService.shutdown();//阻塞,在线程池被关调之前代码不再往下走

scheduledExecutorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

3 使用线程池执行任务

前面提到,任务类型分为有返回值和无返回值的类型,这里的调用也分为有返回值调用和无返回值的调用。

3.1 无返回值任务的调用

如果是无返回值任务的调用,可以用execute或者submit方法,这种情况下二者本质上一样。为了于有返回值任务调用保持统一,建议采用submit方法。

//创建一个线程池

ExecutorService executorService = Executors.newFixedThreadPool(3);//提交一个无返回值的任务(实现了Runnable接口)

executorService.submit(()->System.out.println("Hello"));

executorService.shutdown();

executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

如果有一个任务集合,可以一个个提交。

//创建一个线程池

ExecutorService executorService = Executors.newFixedThreadPool(3);

List tasks =Arrays.asList(

()->System.out.println("Hello"),

()->System.out.println("World"));//逐个提交任务

tasks.forEach(executorService::submit);

executorService.shutdown();

executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

3.2 有返回值任务的调用

有返回值的任务需要实现Callable接口,实现的时候在泛型位置指定返回值类型。在调用submit方法时会返回一个Future对象,通过Future的方法get()可以拿到返回值。这里需要注意的是,调用get()时代码会阻塞,直到任务完成,有返回值。

ExecutorService executorService = Executors.newFixedThreadPool(2);

Future future = executorService.submit(()->"Hello");

System.out.println(future.isDone());//false

String value =future.get();

System.out.println(future.isDone());//true

System.out.println(value);//Hello

如果要提交一批任务,ExecutorService除了可以逐个提交之外,还可以调用invokeAll一次性提交,invokeAll的内部实现其实就是用一个循环逐个提交任务。invokeAll返回的值是一个Future List。

ExecutorService executorService = Executors.newFixedThreadPool(2);

List> tasks = Arrays.asList(()->"Hello", ()->"World");

List> futures = executorService.invokeAll(tasks);

invokeAny方法也很有用,线程池执行若干个实现了Callable的任务,然后返回最先执行结束的任务的值,其它未完成的任务将被正常取消掉不会有异常。如下代码不会输出“Hello”

ExecutorService executorService = Executors.newFixedThreadPool(2);

List> tasks =Arrays.asList(

()->{

Thread.sleep(500L);

System.out.println("Hello");return "Hello";

}, ()->{

System.out.println("World");return "World";

});

String s=executorService.invokeAny(tasks);

System.out.println(s);//World

输出:

World

World

另外,在查看ExecutorService源码时发现它还提供了一个方法  Future submit(Runnable task, T result); ,可以通过这个方法提交一个实现了Runnable接口的任务,然后有返回值,而Runnable接口中的run方法时没有返回值的。那它的返回值是哪来的呢?其实问题在于该submit方法后面的一个参数,这个参数值就是返回的值。调用submit方法之后,有一通操作,然后直接把result参数返回了。

ExecutorService executorService = Executors.newFixedThreadPool(1);

Future future = executorService.submit(() -> System.out.println("Hello"), "World");

System.out.println(future.get());//输出:World

4 小结

在利用多线程处理任务时,应该根据情况选择合适的任务类型和线程池类型。如果无返回值,可以采用实现Runnable或Callable接口的任务;如果有返回值,应该使用实现Callable接口的任务,返回值通过Future的get方法取到。选用线程池时,如果只用1个线程,用单线程池或者容量为1的固定容量线程池;处理大量short-live任务是,使用可缓存的线程池;若要有计划或者循环执行某些任务,可以采用计划任务线程池;如果任务需要消耗大量的CPU资源,应用工作窃取线程池。

java 线程池的使用_Java 使用线程池执行若干任务相关推荐

  1. java中线程池的使用_Java中线程池的简单使用

    什么是线程池? 顾名思义线程池就是线程的容器 举个例子:在没有共享电源的年代,车站有5个人手机都没电且都没有带电源,这五个人想要给手机充电只能去车站的售货亭各花100块钱买一个移动电源:但是现在共享电 ...

  2. java讲对象放在常量池的方法_java的常量池里面都放了些神马东西

    展开全部 理解Java常量池 JVM运行时数据区的内e69da5e6ba9062616964757a686964616f31333264656233存模型由五部分组成: [1]方法区 [2]堆 [3] ...

  3. java中的僵死进程_Java中线程间怎么通讯?什么叫僵死线程?

    <尸家保镖> <猛鬼出千> <不死心灵> <大家发财> <灵幻少女> <九天玄女> <僵尸至尊> <湘西尸王& ...

  4. java实现线程同步的方法_Java实现线程同步方法及原理详解

    一.概述 无论是什么语言,在多线程编程中,常常会遇到多个线同时操作程某个变量(读/写),如果读/写不同步,则会造成不符合预期的结果. 例如:线程A和线程B并发运行,都操作变量X,若线程A对变量X进行赋 ...

  5. java什么时候需要同步_JAVA中线程在什么时候需要同步和互斥

    JAVA中线程在什么时候需要同步和互斥 关注:265  答案:6  mip版 解决时间 2021-01-27 08:10 提问者时光易老 2021-01-27 03:32 JAVA中线程在什么时候需要 ...

  6. java线程安全的方法_Java实现线程安全的方式

    多线程环境中如何保证线程安全?java可以实现线程安全的方式归纳如下: 1.使用synchronized关键字 synchronized关键字可以修饰方法和代码块,它的语义是保证同一段代码同一时间只能 ...

  7. java线程等待都完成_Java等待线程完成

    我有一个线程下载数据,我想等待,直到下载完成之前,我加载数据.有这样做的标准方法吗? 更多信息: 我有一个下载类,从URL(序列化POJO)获取数据.下载是Runnable和Observable.它跟 ...

  8. java的dbcp使用方法_Java dbcp连接池的使用方法

    Java dbcp连接池的使用方法 发布时间:2020-10-31 00:59:26 来源:亿速云 阅读:89 作者:Leah 这篇文章将为大家详细讲解有关Java dbcp连接池的使用方法,文章内容 ...

  9. java list 多条件排序_java – 如何对arraylist执行一系列排序操作(多个排序条件)

    Collections.sort(myList,new Comparator() { @Override public int compare(Object o1,Object o2) { // wr ...

最新文章

  1. R语言使用lmPerm包应用于线性模型的置换方法(置换检验、permutation tests)、使用lm模型构建简单线性回归模型、使用lmp函数生成置换检验回归分析模型
  2. 使用Aspose.Cells的基础知识整理
  3. linux系统如何安装其他包,Linux系统下如何安装软件包
  4. 遗传算法求解极大值问题
  5. 【MFC】自绘对话框动画效果的状态栏
  6. Kubernetes 将何去何从?
  7. php rpoplpush,Redis Rpoplpush 命令
  8. Java从键盘获取两个输入值并计算和
  9. android安卓 通知notification
  10. python抽取html中的链接
  11. QT 实现百万级的数据显示内存消耗几十兆
  12. 作为米粉,我不得不说一说小米11全系,小米还是最初的小米呀
  13. 关于前后台部署打包时需要修改的ip位置
  14. (JAVA编程练习):输入某年某月某日,判断这一天是这一年的第几天?
  15. 李嘉诚能否再续神话?“长科版”上市内幕
  16. SLF4J: The requested version 1.5.8 by your slf4j binding is not compatible with [1.6] SLF4J: See htt
  17. 【Mo&AI TIME 人工智能技术博客】矛与盾的对决——神经网络后门攻防
  18. springboot-EasyExcel
  19. 不仅仅是一把瑞士军刀 —— Apifox的野望和不足
  20. 你的OA,坚持应用了吗?

热门文章

  1. linux init.d 密码,Linux基础之init.d、rc.local
  2. 小米10pro使用说明书_30W有线无线快充,小米的这款立式无线充电宝表现不错
  3. 手机平板巡检系统,掀起设备巡检的第2次革命
  4. linux 安装mysql 5.7.16
  5. 认识div(division)在排版中的作用
  6. 关于 react的生命周期
  7. Apache RewriteCond %{REQUEST_FILENAME} 不起作用问题
  8. nginx配置注意事项1
  9. 谈谈这些年来我为什么一直在坚持
  10. html商城加减号,商城购物车的加减号控制商品数量