比较

在前面的一些文章里,我们已经讨论了手工创建和管理线程。在实际应用中我们有的时候也会经常听到线程池这个概念。在这里,我们可以先针对手工创建管理线程和通过线程池来管理做一个比较。通常,我们如果手工创建线程,需要定义线程执行对象,它实现的接口。然后再创建一个线程对象,将我们定义好的对象执行部分装载到线程中。对于线程的创建、结束和结果的获取都需要我们来考虑。如果我们需要用到很多的线程时,对线程的管理就会变得比较困难。我们手工定义线程的方式在时间和空间效率方面会存在着一些不足。比如说我们定义好的线程不能再进行重复利用,以后每次使用线程的时候都需要去向系统申请资源来创建一个。这样会导致对资源利用效率比较低。另外,我们知道线程的创建和启动都需要一定的时间,每次都从头启动一个线程这样效率也比较低。

以上这些问题恰恰都是线程池所能解决的。笼统的从概念上来说,线程池通过一个缓存池的空间预先创建了一部分线程,在我们需要使用的时候就从里面直接将线程资源取出来使用。在将线程使用完毕之后,线程池又会将线程回收进行再利用。正是因为线程池的这些优点,在一些对性能要求比较高以及线程请求比较多的时候,它是一个很理想的选择,也值得我们好好的研究研究。

Java线程池

线程池类型

自从Java 1.5以来提供的线程池功能,我们使用线程池还是很方便的。一般都是通过Executors类提供的方法来创建。Executors提供了创建一下几类线程池的方法:

  • Single Thread Executor: 创建的线程只包含一个线程,所有提交到线程池的线程会按照提交的顺序一个接一个的执行。通过Executors.newSingleThreadExecutor()方法创建。这种线程池适用于我们只希望每次使用一个线程的情况。
  • Cached Thread Pool: 线程池里会创建尽可能多的必须线程来并行执行。一旦前面的线程执行结束后可以被重复使用。当然,使用这种线程池的时候我们必须要小心。我们使用多线程的目的是希望能够提高并行度和效率,但是并不是线程越多就越好。如果我们设定的线程数目过多的时候,使用Cached Thread Pool并不是一个很理想的选择。因为一方面它占用了大量的线程资源,同时线程之间互相切换很频繁的时候也会带来执行效率的下降。它主要适用于使用的线程数目不多,但是对线程有比较灵活动态的要求。一般通过Executors.newCachedThreadPool()来创建。
  • Fix Thread Pool: 线程池里会创建固定数量的线程。在线程都被使用之后,后续申请使用的线程都会被阻塞在那里。使用Executors.newScheduledThreadPool()创建。
  • Scheduled Thread Pool: 线程池可以对线程执行的时间和顺序做预先指定。比如说要某些线程在某个时候才启动或者每隔多长时间就启动一次。有点像我们的Timer Job。使用Executors.newScheduledThreadPool()创建。
  • Single Thread Scheduled Pool: 线程池按照指定的时间或顺序来启动线程池。同时线程池里只有一个线程。创建方法:Executors.newSingleThreadScheduledExecutor()

前面的这几种类型的线程池已经能够满足我们大多数的要求。从表面上看,这些线程的创建都是通过Executors的静态方法实现。实际上,我们所有创建的线程池都可以说是ExecutorService类型的。前面创建的线程池比如说Cached Thread Pool都是ExecutorService类型的子类。Executors, ExecutorService和ThreadPoolExecutor等各种具体的线程池的类关系图如下:

从这个类图我们可以看到,Executors本身会引用到各种具体ExecutorService的实现。它本身相当于是一个定义的工厂方法,将各种具体线程池的创建给封装起来。当然,我们也可以通过实例化具体的类来建立线程池,只不过相对来说更加麻烦。更加推荐使用Executors的工厂方法。

示例

有了前面那些讨论,我们可以举一个具体的实例来看看线程池的使用。通常来说,我们使用线程可以分为两种类型。一种是使用多个线程执行某些任务,但不一定要将线程执行的结果统一返回并统计。另外一种则需要将线程执行的结果统一记录和统计。我们就针对这两种情况来尝试。

运行线程不统一返回结果

假定我们就通过线程池创建若干个线程,每个线程仅仅是休眠若干秒钟然后打印一些信息。我们可以这样来实现代码:

首先定义要执行的线程:

  1. import java.util.Date;
  2. import java.util.concurrent.TimeUnit;
  3. public class Task implements Runnable {
  4. private Date initDate;
  5. private String name;
  6. public Task(String name) {
  7. initDate = new Date();
  8. this.name = name;
  9. }
  10. @Override
  11. public void run() {
  12. System.out.printf("%s: Task %s: Created on %s\n",
  13. Thread.currentThread().getName(), name, initDate);
  14. System.out.printf("%s: Task %s: Started on %s\n",
  15. Thread.currentThread().getName(), name, new Date());
  16. try {
  17. long duration = (long)(Math.random() * 10);
  18. System.out.printf("%s: Task %s: Doing a task during %d seconds\n",
  19. Thread.currentThread().getName(), name, duration);
  20. TimeUnit.SECONDS.sleep(duration);
  21. } catch(InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. System.out.printf("%s: Task %s: Finished on: %s\n",
  25. Thread.currentThread().getName(), name, new Date());
  26. }
  27. }

这部分代码很简单,就是定义一个Task类,它实现Runnable接口。

然后我们再定义一个封装了线程池的类:

  1. import java.util.concurrent.Executors;
  2. import java.util.concurrent.ThreadPoolExecutor;
  3. public class Server {
  4. private ThreadPoolExecutor executor;
  5. public Server() {
  6. executor = (ThreadPoolExecutor)Executors.newCachedThreadPool();
  7. }
  8. public void executeTask(Task task) {
  9. System.out.printf("Server: A new task has arrived\n");
  10. executor.execute(task);
  11. System.out.printf("Server: Pool Size: %d\n", executor.getPoolSize());
  12. System.out.printf("Server: Active Count: %d\n", executor.getActiveCount());
  13. System.out.printf("Server: Completed Tasks: %d\n", executor.getCompletedTaskCount());
  14. }
  15. public void endServer() {
  16. executor.shutdown();
  17. }
  18. }

Server类主要是内部定义了ThreadPoolExecutor的成员变量和executeTask, endServer两个方法。他们一个用于分配线程并执行,另外一个用于关闭整个线程池。

我们整个使用的代码如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. Server server = new Server();
  4. for(int i = 0; i < 10; i++) {
  5. Task task = new Task("Task " + i);
  6. server.executeTask(task);
  7. }
  8. server.endServer();
  9. }
  10. }

在这里我们通过线程池创建了10个线程。在执行结束之后我们要注意的一点是必须要调用endServer来终止线程池,否则线程池会一直处在一个运行的状态。

总的来说,前面使用线程池的方法非常简单。无非是创建线程执行对象,再将它传入给线程池的execute方法。最后,在使用完毕线程池之后关闭它。

运行线程统一返回结果

现在假定我们要使用一个线程池来调度几个线程。每个线程来计算一个 给定 数字的阶乘n!。那么,我们可以先定义需要提交计算的部分:

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.TimeUnit;
  3. public class FactorialCalculator implements Callable<Integer> {
  4. private Integer number;
  5. public FactorialCalculator(Integer number) {
  6. this.number = number;
  7. }
  8. @Override
  9. public Integer call() throws Exception {
  10. int result = 1;
  11. if(number == 0 || number == 1) {
  12. result = 1;
  13. } else {
  14. for(int i = 2; i <= number; i++) {
  15. result *= i;
  16. TimeUnit.MILLISECONDS.sleep(200);
  17. }
  18. }
  19. System.out.printf("%s: %d\n", Thread.currentThread().getName(), result);
  20. return result;
  21. }
  22. }

这里,我们定义了FactorialCalculator类。它实现了接口Callable。这个接口后面会被ThreadPoolExecutor的submit方法所用到。线程池在收到Callable的参数后会启动一个线程来执行call方法里的运算。还有一个需要注意到的地方是,我们这里定义了返回值是Integer类型的。

现在我们再来看怎么使用他们:

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import java.util.Random;
  4. import java.util.concurrent.ExecutionException;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.Future;
  7. import java.util.concurrent.ThreadPoolExecutor;
  8. public class Main {
  9. public static void main(String[] args) {
  10. //创建固定长度为2的线程池
  11. ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.
  12. newFixedThreadPool(2);
  13. // 声明保存返回结果的列表,注意类型为Future<Integer>
  14. List<Future<Integer>> resultList = new ArrayList<>();
  15. Random random = new Random();
  16. // For循环中的submit方法在提交线程执行后会有一个返回类型为Future<Integer>的结果。将结果保存在列表中。
  17. for(int i = 0; i < 10; i++) {
  18. Integer number = random.nextInt(10);
  19. FactorialCalculator calculator =
  20. new FactorialCalculator(number);
  21. Future<Integer> result = executor.submit(calculator);
  22. resultList.add(result);
  23. }
  24. System.out.printf("Main: Results\n");
  25. for(int i = 0; i < resultList.size(); i++) {
  26. Future<Integer> result = resultList.get(i);
  27. Integer number = null;
  28. try {
  29. // 结果需要在线程执行完后才能get到,所以get执行时会使得线程等待,需要捕捉异常
  30. number = result.get();
  31. } catch(InterruptedException e) {
  32. e.printStackTrace();
  33. } catch(ExecutionException e) {
  34. e.printStackTrace();
  35. }
  36. System.out.printf("Main: Task %d: %d\n", i, number);
  37. }
  38. // 关闭线程池
  39. executor.shutdown();
  40. }
  41. }

前面的代码我增加了一些说明的注释。其中的要点就是我们submit提交运算之后并不会马上得到结果。但是这个结果集我们可以将他们保存到一个列表中。在后面通过get的方法来获取。

这里还有一个比较有意思的地方就是,在前面那种不返回结果的地方,我们是通过提交一个Runnable的参数给ThreadPoolExecutor的execute方法来安排线程执行。这个我们好理解。可是在后面这个返回结果的地方,我们是提交了一个Callable的参数给ThreadPoolExecutor的submit方法。这里没有声明线程的东西,比如说我实现Runnable或者集成Thread,我怎么知道分配线程来执行call方法里面的东西呢? 这个其实在ThreadPoolExecutor继承的类结构里有一个转换的方式,将Callable的变量绑定到一个线程对象实例当中,然后启动线程执行。在后续线程池详细的实现分析中我们再深入讨论。

综合这部分的讨论,返回结果集其实也就那么回事。不就是定义一个实现Callable的对象提交给线程池的submit方法么?后面我们只要等着去读submit返回的Future<T>结果就行了。So easy!

Java线程池和线程数的选择

我们使用线程池来做多线程运算的时候,问题类型和对应线程数量的选择都很重要。一般来说,我们面临的多线程处理问题会分为如下两类:

CPU密集

对于CPU密集类型的问题来说,更多只是CPU要做大量的运算处理。这个时候如果要保证系统最充分的利用率,我们最好定义的线程数量和系统的CPU数量一致。这样每个CPU都可以充分的运用起来,而且很少或者不会有线程之间的切换。在这种情况下,比较理想的线程数目是CPU的个数或者比CPU个数大1。一般来说,我们可以通过如下的代码来得到CPU的个数:

  1. Runtime.getRuntime().availableProcessors();

IO密集

对于IO密集型的问题来说,我们会发现由于IO经常会带来各种中断,并且IO的速度和CPU处理速度差别很大。因此,我们需要考虑到IO在整个运算时间中所占的比例。比如说我们有50%比例的时间是阻塞的,那么我们可以创建相当于当前CPU数目的两倍数的线程。假设我们知道阻塞的比例数的话,我们可以通过如下的一个简单关系来估算线程数的大小:

线程数 = 可用CPU个数 / (1 - 阻塞比例数)

当然,这种估算的方式是基于一个理想的条件,在实际问题中还需要根据具体场景来调整。

总结

和手动创建和管理线程比起来,使用线程池来管理线程是一种更加理想的选择。我们可以根据具体问题对线程的要求来选择不同的线程池。创建线程池一般推荐使用Executors里定义的工厂方法,它本身屏蔽了很多创建线程池的细节。另外,我们通常使用线程池来启用多个线程的时候,有的时候不需要去刻意统计每个线程执行的结果,有的时候又需要。可以笼统的将线程池的使用分成这两类。在我们不需要统计结果的时候可以直接用线程池里ThreadPoolExecutor的execute(Runnable command)方法来执行线程。如果我们需要获得线程执行的结果,可以使用ThreadPoolExecutor的submit(Callable call)来提交线程,然后通过收集submit返回的Future<T>结果进行统计。

另外,我们选择线程池执行多任务的时候也需要考虑执行的任务类型。通常这些任务可以分为CPU密集和IO密集的两种类型。对于CPU密集的任务,我们应该选择和CPU数量一致的线程数,这样可以达到理想的执行效果也减少了线程切换的开销。对于IO密集的任务,我们可以根据IO所占程序执行中的比例来选择,一般推荐线程数目是CPU数目的10倍。

java线程池分析和应用的更多相关文章

  1. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  2. Java 线程池框架核心代码分析

    前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和资源消耗都是很高的.线程池应运而生,成为我们管理线程的利器.Java 通过Executor接口,提供了一种标准的方法将任务的提交过 ...

  3. Java线程池使用和分析(一)

    线程池是可以控制线程创建.释放,并通过某种策略尝试复用线程去执行任务的一种管理框架,从而实现线程资源与任务之间的一种平衡. 以下分析基于 JDK1.7 以下是本文的目录大纲: 一.线程池架构 二.Th ...

  4. Java线程池使用和分析(二) - execute()原理

    相关文章目录: Java线程池使用和分析(一) Java线程池使用和分析(二) - execute()原理 execute()是 java.util.concurrent.Executor接口中唯一的 ...

  5. Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

  6. java线程池ThreadPoolExector源码分析

    java线程池ThreadPoolExector源码分析 今天研究了下ThreadPoolExector源码,大致上总结了以下几点跟大家分享下: 一.ThreadPoolExector几个主要变量 先 ...

  7. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  8. Java 线程池(ThreadPoolExecutor)原理分析与使用

    在我们的开发中"池"的概念并不罕见,有数据库连接池.线程池.对象池.常量池等等.下面我们主要针对线程池来一步一步揭开线程池的面纱. 使用线程池的好处 1.降低资源消耗 可以重复利用 ...

  9. Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

随机推荐

  1. android-通知Notification

    发送通知 public class MyActivity extends Activity { @Override protected void onCreate(Bundle savedInstan ...

  2. cocos2dx CCControlSlider

    有的同学建议先上图,好吧,先上效果图 再看代码,创建了两个CCControlSlider在主窗口中 // on "init" you need to initialize your ...

  3. C++STL之string (转)

    在学习c++STL中的string,在这里做个笔记,以供自己以后翻阅和初学者参考. 1:string对象的定义和初始化以及读写 string s1;      默认构造函数,s1为空串 string ...

  4. Git跨平台中文乱码临时解决方案

    Git 是一个非常优秀的分布式版本控制系统,最初为Linux Kernel版本管理进行量身定做.优点是,和其他版本控制系统相比,稳定,速度快,跨平台,易学易用,无需要花费成本.更多优点请点击阅读:ht ...

  5. java内存映射文件

    内存映射文件能够让我们创建和修改大文件(大到内存无法读入得文件),对于内存映射文件,我们可以认为是文件已经全部被读入到内存当中,然后当成一个大的数字来访问,简化修改文件的代码. 1.directBuf ...

  6. git学习基础教程

    分享一个git学习基础教程 http://pan.baidu.com/s/1o6ugkGE 具体在网盘里面的内容..需要的学习可以直接下.

  7. 关于playframework2.5

    加入了很多新东西: 1.用akka streams 替换了大部分 iteratee-based async io,当然还有一些模块在用iteratees 2.java 的一些API 做了调整升级,以及 ...

  8. RAW模板开发必备知识

    写这个主要是为了让已经熟练掌握PHP的人能够快速的掌握RAW模板开发,从而享受RAW的优越! (注:在实际开发中,最好注意RAW模板开发统一规范,那样可以增强用户体验) 废话不多说,进入正题. 需要记 ...

  9. hdu 4614 Vases and Flowers 线段树

    题目链接 一共n个盒子, 两种操作, 第一种是给出两个数x, y, 从第x个盒子开始放y朵花, 一个盒子只能放一朵, 如果某个盒子已经有了, 那么就跳过这个盒子放下面的盒子. 直到花放完了或者到了最后 ...

  10. Python 爬取CSDN博客频道

    初次接触python,写的很简单,开发工具PyCharm,python 3.4很方便 python 部分模块安装时需要其他的附属模块之类的,可以先 pip install wheel 然后可以直接下载 ...