初步理解线程池

一、什么是线程池?

线程池(Thread Pool)是一种基于池化思想管理线程的工具。 该工具可以同时处理多个线程所提交的任务,是Java中运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,经常出现在多线程服务器中,如MySQL。

二、为何要使用线程池?

因为线程的创建和销毁会带来一定的资源开销,线程池的主要目的就是为了减少每次获取资源和释放资源的消耗,提高对资源的利用率。

通俗地说,就是在一个”池子“里提前创建好多个线程,当需要线程执行任务时,就从这个“池子”中拿一些线程出来操作任务,等任务操作完成后,再将线程归还到线程池中,从而免去了对新任务新建线程的消耗,以达到线程复用的这一目的。

总的来说,合理的使用线程池能够带来3个好处:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不用等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

以上3个好处参考自《Java并发编程的艺术》。

三、如何使用线程池?

3.1 不建议使用的四种线程池

在早期,Java通过工具类Executors来实现四种线程池,分别如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo1 {
    public static void main(String[] args) {
        // Executors创建的四种线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        //ExecutorService threadPool = Executors.newScheduledThreadPool(2);

        // 创建5个线程,交给线程池执行。
        try {
            for(int i=0; i<5; i++){
            	// 调用线程池的execute方法来执行任务
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " hello");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

// 输出结果为:(此处为2个线程数的newFixedThreadPool)
pool-1-thread-2 hello
pool-1-thread-1 hello
pool-1-thread-2 hello
pool-1-thread-1 hello
pool-1-thread-2 hello

而这四个线程池的实现,底层都是基于ThreadPoolExecutor来实现的,根据《阿里巴巴开发手册》中的规定,不允许使用Executors去创建,而是通过ThreadPoolExecutor 的方式实现,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

具体原因:

Executors创建的线程池有弊端:

  • FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

3.2 使用ThreadPoolExecutor来创建线程池

(1)ThreadPoolExecutor用法

在这里插入图片描述

(2)execute() 和 submit() 方法

可以使用两个方法向线程池提交任务,分别是execute()和submit()。

1、 execute():该方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

execute()的使用示例

// 在execute方法里传入Runnable接口的实现。
threadPool.execute(new Runnable(){
		@Override
		public void run(){
			// TODO 线程需要执行的代码
		}
});

execute()源码
在这里插入图片描述

2、submit():该方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

submit()的使用示例

submit()方法的使用示例可以参考这篇博文:Java多线程-线程池ThreadPoolExecutor的submit返回值Future

四、ThreadPoolExecutor原理解析

4.1 ThreadPoolExecutor继承关系图

在这里插入图片描述
从图中可知ThreadPoolExecutor是继承自AbstractExecutorService的。

4.2 ThreadPoolExecutor的构造方法

在这里插入图片描述

4.3 ThreadPoolExecutor中7大参数

接下来分别介绍下这7个参数所代表的含义。

  • 1、corePoolSize:线程池中的常驻核心线程数。

  • 2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,如果阻塞队列满了,并且已经创建的线程数大于corePoolSize且小于maximumPoolSize,则线程池会创建新的线程执行任务。注意:如果是无界的阻塞队列,则这个参数没有意义。
    设置
        如果任务是CPU密集型的,那maximumPoolSize设置成电脑CPU核数+1;通过代码
    System.out.println(Runtime.getRuntime().availableProcessors());来获取电脑CPU的核数。
        如果任务是I/O密集型的,那maximumPoolSize设置成(待弄明白。。。)

  • 3、keepAliveTime多余的空闲线程的存活时间。当前线程池中线程数量超过corePoolSize时,且空闲时间达到keepAliveTime时,多余线程会被销毁,直到剩下corePoolSize个线程为止。

  • 4、unit:keepAliveTime的时间单位。可以是秒、毫秒等。

  • 5、workQueue:任务阻塞队列。被提交但尚未被执行的任务。

    • 阻塞队列是用于保存等待执行的任务的。可以选择以下几种:
      (1) ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,符合FIFO原则。
      (2) LinkedBlockingQueue:是一个基于链表结构的阻塞队列,符合FIFO原则,吞吐量通常要高于ArrayBlockingQueue。
      (3) SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
      (4) PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • 6、threadFactory:生成线程池中工作线程的线程工厂,用于创建线程。一般默认

  • 7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于maximumPoolSize时(当请求的线程数 > maximumPoolSize + BlockingQueue.capacity时) ,该策略来拒绝执行runnable的请求。可以通过ThreadPoolExecutor.XxxPolicy调用。

    • 拒绝策略有以下四种:
      (1)AbortPolicy(默认):直接抛出RejectExecutionException异常阻止系统正常运行。
      (2)CallerRunsPolicy:该策略不会抛弃任务,也不会抛出异常,而是将任务回退到调用者,从而降低新任务的流量。
      (3)DiscardPolicy:该策略默认丢弃无法处理的任务,不予任何处理也不抛出异常,如果任务允许抛弃,这是最好的策略。
      (4)DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加到队列中尝试再次提交。

初看起来感觉很多,很复杂,不仅仅有7个参数,参数也还有可选项。这里我举一个贴近生活的、通俗易懂的例子来帮组理解和记忆。

银行办理业务
当前时间段来银行办理业务的人 <=> 创建的线程数
当前常开的2个业务窗口 <=> corePoolSize(2)
银行一共有5个办业务的窗口(包括常开的2个) <=> maximumPoolSize(5)
业务窗口的空闲时间 <=> keepAliveTime
大厅用于排队等待时提供给客户坐的凳子 <=> workQueue
如果今天人已经达到了上限,那保安在门口贴的告示,告诉你下次再来,还是去其他网点。。。 <=> handler

4.4 ThreadPoolExecutor的执行流程

当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?可以从图中看出。
在这里插入图片描述
图片来源于《Java并发编程的艺术》。

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

(1)线程池判断核心线程池 (corePoolSize) 里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
(2)线程池判断工作队列是否已经满。如果工作队列 (BlockingQueue) 没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
(3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值