1.为什么使用线程池

在多线程编程中一项很重要的功能就是执行任务,而执行任务的方式有很多种,为什么一定需要使用线程池呢?下面我们使用Socket编程处理请求的功能,分别对每种执行任务的方式进行分析。

1.1串行执行任务

当Socket监听到客户端有连接,通过handleSocket方法顺序的处理每一个客户端连接,当处理完成后,继续监听。代码如下:

  1. ServerSocket serverSocket = new ServerSocket();
  2. SocketAddress endpoint = new InetSocketAddress(host, port);
  3. serverSocket.bind(endpoint,1023);
  4. while (!isStop) {
  5. Socket socket = serverSocket.accept();
  6. handleSocket(socket);
  7. }

这种方式的缺点非常明显:当我有多个客户端请求时,在server处理一个请求的过程中,其他请求都需要等待前一个请求处理完毕。这种在高并发情况下几乎不可用。

1.2为每个任务创建一个线程

针对上面的问题进行优化:为每一个客户端请求创建一个线程来处理请求,主线程只需要创建线程,之后即可继续坚挺客户端请求.流程图如下:

代码如下:

  1. ServerSocket serverSocket = new ServerSocket();
  2. SocketAddress endpoint = new InetSocketAddress(host, port);
  3. serverSocket.bind(endpoint,1023);
  4. while (!isStop) {
  5. Socket socket = serverSocket.accept();
  6. new SocketHandler(socket, THREAD_NAME_PREFIX + threadIndex++).start();
  7. }

这种方式有以下优点:

1.将处理客户端连接的操作从主线程中分离出去,使得主循环可以更快的响应下一次请求。

2.处理客户端连接的操作是并行的,提高了程序的吞吐量。

但是这种方式有有以下几个缺点:

1.处理请求的线程必须是线程安全的

2.线程的创建和销毁都需要开销,当大量创建线程的时候,将会消耗大量计算机资源

3.当可用的CPU数量小于可运行的线程的时候,那么多出来的线程会占用内存资源,给垃圾回收带来压力,并且在大量线程竞争CPU资源的时候会有很大的性能开销

4.JVM中可创建的线程数存在一个上限,这个上限随着平台的不同而不同,并且受多个因素的限制,包括JVM的启动参数,每个线程所占用的内存大小等,如果超出这些限制,将会抛出OOM异常。

1.3 使用线程池处理客户端请求

对于1.2中出现的问题,最好的解决方案就是使用线程池来执行task,这样可以对创建的线程总数做限制,从而避免1.2中的问题。流程图如下:

处理方式如下:

  1. ServerSocket serverSocket = new ServerSocket();
  2. SocketAddress endpoint = new InetSocketAddress(host, port);
  3. serverSocket.bind(endpoint,1023);
  4. while (!isStop) {
  5. Socket socket = serverSocket.accept();
  6. executorService.execute(new SocketHandler(socket, THREAD_NAME_PREFIX + threadIndex++));
  7. }

此中方式有以下几个优点:

1.任务提交和任务执行分离开

2.执行任务的线程可以重用,减少了线程创建和销毁的开销,同时当任务到达时可以直接使用创建好的线程执行任务,也提高了程序的响应速度。

2.java中线程池介绍

在java中线程池的实现是基于生产者-消费者模式的,线程池的功能将任务的提交和任务的执行分离,任务提交的过程为生产者,执行任务的过程为消费过程。具体的分析见源码分析。java线程池的顶层接口为Executor,源码如下:

  1. public interface Executor {
  2. void execute(Runnable command);
  3. }

此接口为所有线程池实现的顶层接口,其规定了可以接受的task类型为Runnable实现类,但是具体的执行task的逻辑由线程池实现类自己定义,比如:

可以使用主线程串行执行任务,

也可以为每个任务创建一个新的线程

或者提前创建好一组线程,每次执行任务的时候从一组线程中取,等等

对于线程池的执行策略主要有以下几个方面:

1.在什么线程中执行任务

2.按照什么顺序执行任务(FIFO、LIFO、优先级?)

3.有多少个任务可以并发执行

4.最多可以有多少个任务在队列中等待执行

5.当等待队列中达到最大值的时候,怎么样拒绝新提交的task

6.在执行一个任务之前或者之后需要做哪些操作?

应该根据具体的业务选择不同的执行策略。在java类库中提供了Executors工具类来常见默认策略的线程池。主要有以下几个接口:

  1. public static ExecutorService newFixedThreadPool(int nThreads)
  2. 将会创建一个固定大小的线程池,每当有新任务提交的时候,当线程总数没有达到核心线程数的时候,为每个任务创建一个新线程,当线程的个数到达最大值后,重用之前创建的线程,当线程因为未知异常而停止时候,将会重现创建一个线程作为补充。
  3. public static ExecutorService newCachedThreadPool()
  4. 根据需求创建线程的个数,当线程数大于任务数的时候,将会注销多余的线程
  5. public static ExecutorService newSingleThreadExecutor()
  6. 创建一个单线程的线程池
  7. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
  8. 创建一个可执行定时任务的线程池

在以上的例子中,所有提交的task在提交到线程池后其执行状态是不可见的,即主线程无法知道提交的task是否执行结束或者执行结果。针对这个问题,java提供了可以返回数据的task接口Future和Callable接口。

其中Callable接口提供了任务返回数据以及抛出异常的功能,定义如下:

  1. public interface Callable<V> {
  2. V call() throws Exception;
  3. }

在ExecutorService中所有的submit方法都会返回一个Future对象,其接口定义如下:

  1. public interface Future<V> {
  2. 取消任务执行,当mayInterruptIfRunningtrueinterruptedthisthread
  3. boolean cancel(boolean mayInterruptIfRunning);
  4. 返回此任务是否在执行完毕之前被取消执行
  5. boolean isCancelled();
  6. 返回此任务是否已经完成,包括正常结束,异常结束以及被cancel
  7. boolean isDone();
  8. 返回执行结果,当任务没有执行结束的时候,等待
  9. V get() throws InterruptedException, ExecutionException;
  10. }

3.使用线程池可能出现的问题

1.线程饥饿死锁

在单线程的Executor中,如果Executor中执行的一个任务中,再次提交任务到同一个Executor中,并且等待这个任务执行完毕,那么就会发生死锁问题。如下demo中所示:

  1. public class ThreadDeadLock {
  2. private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
  3. public static void main(String[] args) throws Exception {
  4. System.out.println("Main Thread start.");
  5. EXECUTOR_SERVICE.submit(new DeadLockThread());
  6. System.out.println("Main Thread finished.");
  7. }
  8. private static class DeadLockThread extends Thread{
  9. @Override
  10. public void run() {
  11. try {
  12. System.out.println("DeadLockThread start.");
  13. Future future = EXECUTOR_SERVICE.submit(new DeadLockThread2());
  14. future.get();
  15. System.out.println("DeadLockThread finished.");
  16. } catch (Exception e) {
  17. }
  18. }
  19. }
  20. private static class DeadLockThread2 extends Thread {
  21. @Override
  22. public void run() {
  23. try {
  24. System.out.println("DeadLockThread2 start.");
  25. Thread.sleep(1000 * 10);
  26. System.out.println("DeadLockThread2 finished.");
  27. } catch (Exception e) {
  28. }
  29. }
  30. }
  31. }

输出结果为:

  1. Main Thread start.
  2. Main Thread finished.
  3. DeadLockThread start.

对于多个线程的线程池,如果所有正在执行的线程都因为等待处于工作队列中的任务执行而阻塞,那么就会发生线程饥饿死锁。

当往线程池中提交有依赖的任务时,应清楚的知道可能会出现的线程饥饿死锁风险。应考虑是否将依赖的task提交到不同的线程池中

或者使用无界的线程池。

只有当任务相对独立时,设置线程池大小和工作队列的大小才是合理的,否则有可能会出现线程饥饿死锁

2.任务运行时间过长

任务执行时间过长会影响线程池的响应时间,当运行时间长的任务远大于线程池线程的个数时,会出现所有线程都在执行运行时间长的任务,从而影响对其他任务的响应。

解决办法:

1.通过限定任务等待的时长,而不要无限期等待下去,当等待超时的时候,可以将任务标记为失败,或者重新放到线程池中。

2.当线程池中阻塞任务过多的时,应该考虑扩大线程池的大小

4.线程池大小的设置

线程池的大小依赖于提交任务的类型以及服务器的可用资源,线程池的大小应该避免设置过大或者过小,当线程设置过打的时候可能会有资源耗尽的风险,线程池设置过小会有可用cpu空闲从而影响系统吞吐量。

影响线程池大小的资源有很多,比如CPU、内存、数据库链接池等,只需要计算资源可用总资源 / 每个任务需要的资源,取最小值,即可得出线程池的上限。

线程池的最小值应该大于可用的CPU数量。

4.java中常用线程池源码分析-ThreadPoolExecutor

ThreadPoolExecutor线程池是比较常用的一个线程池实现类,通过Executors工具类创建的线程池中,其具体实现类是ThreadPoolExecutor。首先我们可以看下ThreadPoolExecutor的构造函数如下:

  1. public ThreadPoolExecutor(int corePoolSize,
  2. int maximumPoolSize,
  3. long keepAliveTime,
  4. TimeUnit unit,
  5. BlockingQueue<Runnable> workQueue,
  6. ThreadFactory threadFactory,
  7. RejectedExecutionHandler handler)

下面分别对构造函数中的各个参数对应的策略进行分析:

1.线程的创建与销毁

首先构造函数中corePoolSize、maximumPoolSize、keepAliveTime和unit参数影响线程的创建和销毁。其中corePoolSize为核心线程数,当第一次提交任务的时候如果正在执行的线程数小于corePoolSize,则新建一个线程执行task,如果已经超过corePoolSize,则将任务放到任务队列中等待执行。当任务队列的个数到达上限的时候,并且工作线程数量小于maximumPoolSize,则继续创建线程执行工作队列中的任务。当任务的个数小于maximumPoolSize的时候,将会把空闲的线程标记为可回收的垃圾线程。对于以下代码段测试此功能:

  1. public class ThreadPoolTest {
  2. private static ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 6,100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3));
  3. public static void main(String[] args) throws Exception {
  4. for (int i = 0; i< 9; i++) {
  5. executorService.submit(new Task());
  6. System.out.println("Active thread:" + executorService.getActiveCount() + ".Task count:" + executorService.getTaskCount() + ".TaskQueue size:" + executorService.getQueue().size());
  7. }
  8. }
  9. private static class Task extends Thread {
  10. @Override
  11. public void run() {
  12. try {
  13. Thread.sleep(1000 * 100);
  14. } catch (Exception e) {
  15. }
  16. }
  17. }
  18. }

输出结果为:

  1. Active thread:1.Task count:1.TaskQueue size:0
  2. Active thread:2.Task count:2.TaskQueue size:0
  3. Active thread:3.Task count:3.TaskQueue size:0
  4. Active thread:3.Task count:4.TaskQueue size:1
  5. Active thread:3.Task count:5.TaskQueue size:2
  6. Active thread:3.Task count:6.TaskQueue size:3
  7. Active thread:4.Task count:7.TaskQueue size:3
  8. Active thread:5.Task count:8.TaskQueue size:3
  9. Active thread:6.Task count:9.TaskQueue size:3

2.任务队列

在ThreadPoolExecutor的构造函数中可以传入保存任务的队列,当新提交的任务没有空闲线程执行时候,会将task保存到此队列中。保存的顺序是根据插入的顺序或者Comparator来排序的。

3.饱和策略

  1. ThreadPoolExecutor.AbortPolicy
  2. 抛出RejectedExecutionException
  3. ThreadPoolExecutor.CallerRunsPolicy
  4. 将任务的执行交给调用者,即将本该异步执行的任务变成同步执行。

4.线程工厂

当线程池需要创建线程的时候,默认是使用线程工厂方法来创建线程的,通常情况下我们通过指定线程工厂的方式来为线程命名,便于出现线程安全问题时候来定位问题。

6.线程池最佳实现

1.项目中所有的线程应该都有线程池来提供,不允许自行创建线程

2.尽量不要用Executors来创建线程,而是使用ThreadPoolExecutor来创建

Executors有以下问题:

  1. 1FixedThreadPool SingleThreadPool:
  2. 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
  3. 2CachedThreadPool ScheduledThreadPool:
  4. 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

Java线程池使用和源码分析的更多相关文章

  1. java线程池ThreadPoolExector源码分析

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

  2. [转载] Java线程池框架源码分析

    转载自http://www.linuxidc.com/Linux/2014-11/108791.htm 相关类Executor,Executors,AbstractExecutorService,Ex ...

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

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

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

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

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

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

  6. Java调度线程池ScheduledThreadPoolExecutor源码分析

    最近新接手的项目里大量使用了ScheduledThreadPoolExecutor类去执行一些定时任务,之前一直没有机会研究这个类的源码,这次趁着机会好好研读一下. 该类主要还是基于ThreadPoo ...

  7. 深入浅出Java线程池:源码篇

    前言 在上一篇文章深入浅出Java线程池:理论篇中,已经介绍了什么是线程池以及基本的使用.(本来写作的思路是使用篇,但经网友建议后,感觉改为理论篇会更加合适).本文则深入线程池的源码,主要是介绍Thr ...

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

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

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

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

随机推荐

  1. PHP与MYSQL结合操作——文章发布系统小项目(实现基本增删查改操作)

    php和mysql在一起几十年了,也是一对老夫老妻了,最近正在对他们的爱情故事进行探讨,并做了一个很简单的小东西——文章发布系统,目的是为了实现mysql对文章的基本增删查改操作 前台展示系统有:文章 ...

  2. Coursera 算法二 week2 Seam Carving

    这周作业设计到的算法是有向无环图的最短路径算法,只需要按照顶点的拓扑顺序去放松顶点即可.而在这个题目中拓扑顺序就是按照行的顺序或列的顺序. 用到的数据结构为一个二维数组picture同来存储每个像素的 ...

  3. IOS 截屏(保存到相册中)

    @interface NJViewController () /** * 点击截屏按钮 */ - (IBAction)captureView:(UIButton *)sender; /** * 白色v ...

  4. bzoj3312: [Usaco2013 Nov]No Change

    题意: K个硬币,要买N个物品.K<=16,N<=1e5 给定买的顺序,即按顺序必须是一路买过去,当选定买的东西物品序列后,付出钱后,货主是不会找零钱的.现希望买完所需要的东西后,留下的钱 ...

  5. 2018.5.25 Oracle相关的函数命令

    第03章 函数 1 Oracle的函数 Oracle的函数和java中的方法一样, 能完成一定的功能 2 字符处理类函数 --需求1:把ename字段转换成小写 select lower(ename) ...

  6. Linux 的歷史

    Unix 狹義作業系統提供應用程式及命令直譯器. 作業系統發展初期並不具可攜性. Bell, GE 及 MIT 合作開發的 "Multice" 系統( 相容分時系統 ). 1969 ...

  7. 51+Nokia5110

    #include<reg52.h> #include <intrins.h> #define uchar unsigned char #define uint unsigned ...

  8. PAT (Basic Level) Practise (中文)- 1006. 换个格式输出整数 (15)

    http://www.patest.cn/contests/pat-b-practise/1006 让我们用字母B来表示“百”.字母S表示“十”,用“12...n”来表示个位数字n(<10),换 ...

  9. komodo-edit

    sudo add-apt-repository ppa:mystic-mirage/komodo-edit sudo apt-get update sudo apt-get install komod ...

  10. jeesite项目

    1,登录页面.最高权限管理员 用户名:thinkgem  密码:admin 2,登陆之后展示的首页 3,分为不同的模块,由不同的入负责,我负责日志管理 已完成功能:模糊查询,分页,导入,导出Excel ...