Java线程池原理与实战详解

简介: 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因。比如大家所熟悉的数据库连接池正是遵循这一思想而产生的!

前言

分割线.jpg

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因。比如大家所熟悉的数据库连接池正是遵循这一思想而产生的!


image.png


线程池是什么?


简单来说,线程池是指提前创建若干个线程,当有任务需要处理时,线程池里的线程就会处理任务,处理完成后的线程并不会被销毁,而是继续等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以,当某个业务需要频繁进行线程的创建和销毁时,就可以考虑使用线程池来提高系统的性能啦。


线程池可以做什么?


借由《Java并发编程的艺术》这本书,使用线程池能够帮助我们 :

降低资源消耗。通过重复利用已经创建的线程,能够降低线程创建和销毁造成的消耗。

提高响应速度。当任务到达时,任务可以不需要等待线程的创建就能立即执行。

提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。


如何创建一个线程池


首先创建一个 Runnable 接口实现类。

packagedemo;
importjava.util.Date;
publicclassDemoThreadimplementsRunnable{
privateString command;
publicDemoThread(String command){
this.command = command;
}@Override
publicvoidrun(){
System.out.println(Thread.currentThread().getName() +" 开始时间 : "+newDate());
processCommand();        System.out.println(Thread.currentThread().getName() +" 结束时间 : "+newDate());
}privatevoidprocessCommand(){
try{
Thread.sleep(5000);
}catch(InterruptedException e) {
e.printStackTrace();        }    }@Override
publicStringtoString(){
return"DemoThread{"+
"command='"+ command +'\''+
'}';
}}

这里让我们使用 ThreadPoolExecutor 来创建一个线程池进行测试:

packagedemo;
importjava.util.concurrent.ArrayBlockingQueue;
importjava.util.concurrent.ThreadPoolExecutor;
importjava.util.concurrent.TimeUnit;
publicclassDemoThreadPoolExecutor{
privatestaticfinalintCORE_POOL_SIZE =5;
privatestaticfinalintMAX_POOL_SIZE  =10;
privatestaticfinalintQUEUE_CAPACITY =100;
privatestaticfinalLong KEEP_ALIVE_TIME =1L;
publicstaticvoidmain(String[] args){
// 使用线程池来创建线程
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程数为 :5
                CORE_POOL_SIZE,
                // 最大线程数 :10
                MAX_POOL_SIZE,
                // 等待时间 :1L
                KEEP_ALIVE_TIME,
                // 等待时间的单位 :秒
                TimeUnit.SECONDS,
                // 任务队列为 ArrayBlockingQueue,且容量为 100
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                // 饱和策略为 CallerRunsPolicy
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        for(int i = 0; i < 15; i++) {
            // 创建WorkerThread对象,该对象需要实现Runnable接口
            Runnable worker = new DemoThread("任务" + i);
            // 通过线程池执行Runnable
            threadPoolExecutor.execute(worker);
        }
        // 终止线程池
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
        }
        System.out.println("全部线程已终止");
    }
}

最后让我们来看一下运行结果 :

image.png可以看到,当核心线程数为 5 时,即使总共要运行的线程有 15 个,每次也只会同时执行 5 个任务,剩下的任务则会被放入等待队列,等待核心线程空闲后执行。总的来说步骤如下 :

image.png

Executor框架


Executor 框架是 Java5 之后引进的。在 Java5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好。除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点 :有助于避免 this 逃逸问题。


this 逃逸


this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法时可能引发奇怪的错误。

引发 this 逃逸通常需要满足两个条件 :一个是在构造函数中创建内部类,另一个就是在构造函数中将这个内部类发布了出去。

由于发布出去的内部类对象自带对外部类 this 的访问权限,这就导致在通过内部类对象访问外部类 this 时,外部类可能并未构造完成,从而导致一些意想不到的问题。

典型的 this 逃逸情景如下 :

publicclassDemoThisEscape{
privateinta =10;
publicDemoThisEscape(){
// 在外部类的构造函数中调用内部类
        new Thread(new InnerClass()).start();
    }
    private class InnerClass implements Runnable {
        @Override
        public void run() {
            // 在这里通过 DemoThisEscape.this 引用尚未构造完毕的对象,比如这样 :
            System.out.println(DemoThisEscape.this.a);
        }
    }
}

通过使用线程池进行统一的线程调度,省去了在程序中手动启动线程的步骤,从而避免了在构造器中启动一个线程的情况,因此能够有效规避 this 逃逸。


ThreadPoolExecutor常用参数


1. corePoolSize :核心线程线程数

定义了最小可以同时运行的线程数量。

2. maximumPoolSize :最大线程数

当队列中存放的任务达到队列容量时,当前可以同时运行的线程数量会扩大到最大线程数。

3. keepAliveTime :等待时间

当线程数大于核心线程数时,多余的空闲线程存活的最长时间。

4. unit :时间单位。

keepAliveTime 参数的时间单位,包括 TimeUnit.SECONDS、TimeUnit.MINUTES、TimeUnit.HOURS、TimeUnit.DAYS 等等。

5. workQueue :任务队列

任务队列,用来储存等待执行任务的队列。

6. threadFactory :线程工厂

线程工厂,用来创建线程,一般默认即可。

7. handler :拒绝策略

也称饱和策略;当提交的任务过多而不能及时处理时,可以通过定制策略来处理任务。


ThreadPoolExecutor 饱和策略 : 指当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,ThreadPoolTaskExecutor 所执行的策略。

常用的拒绝策略包括 :

ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException 来拒绝新任务的处理,是 Spring 中使用的默认拒绝策略。

ThreadPoolExecutor.CallerRunsPolicy: 线程调用运行该任务的 execute 本身,也就是直接在调用 execute 方法的线程中运行 (run) 被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度,但可能造成延迟。若应用程序可以承受此延迟且不能丢弃任何一个任务请求,可以选择这个策略。

ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。

ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。


为什么推荐使用 ThreadPoolExecutor 来创建线程?


规约一 :线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

规约二 :强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM。

CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。

几种常见的线程池

FixThreadPool 固定线程池

FixThreadPool :可重用固定线程数的线程池。

publicstaticExecutorServicenewFixedThreadPool(intnThreads, ThreadFactory threadFactory){
returnnewThreadPoolExecutor(
nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
newLinkedBlockingQueue(),
threadFactory);    }


执行机制 :


若当前运行的线程数小于 corePoolSize,来新任务时,就创建新的线程来执行任务;

当前运行的线程数等于 corePoolSize 后,如果再来新任务的话,会将任务加到 LinkedBlockingQueue;

线程池中的线程执行完手头的工作后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行。

FixThreadPool 使用的是无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),而它会给线程池带来如下影响 :

当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;

由于使用的是一个无界队列,所以 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况,所以 FixedThreadPool 的 corePoolSize、maximumPoolSize 被设置为同一个值,且 keepAliveTime 将是一个无效参数;

运行中的 FixedThreadPool(指未执行 shutdown() 或 shutdownNow() 的)不会拒绝任务,因此在任务较多的时候可能会导致 OOM。


SingleThreadExecutor 单一线程池


SingleThreadExecutor 是只有一个线程的线程池。

publicstaticExecutorServicenewSingleThreadExecutor(ThreadFactory threadFactory){
returnnewFinalizableDelegatedExecutorService
(newThreadPoolExecutor(
1,1,
0L, TimeUnit.MILLISECONDS,
newLinkedBlockingQueue(),
threadFactory));}

除了池中只有一个线程外,其他和 FixThreadPool 是基本一致的。


CachedThreadPool 缓存线程池


CachedThreadPool 是一个会根据需要创建新线程的线程池,但会在先前构建的线程可用时重用它。

publicstaticExecutorServicenewCachedThreadPool(ThreadFactory threadFactory){
returnnewThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
newSynchronousQueue(),
threadFactory);}

其 corePoolSize 被设置为 0,maximumPoolSize 被设置为 Integer.MAX.VALUE,也就是无界的。虽然是无界,但由于该线程池还存在一个销毁机制,即如果一个线程 60 秒内未被使用过,则该线程就会被销毁,这样就节省了很多资源。

但是,如果主线程提交任务的速度高于 maximunPool 中线程处理任务的速度,CachedThreadPool 将会源源不断地创建新的线程,从而依然可能导致 CPU 耗尽或内存溢出。

执行机制 :

首先执行 offer 操作,提交任务到任务队列。若当前 maximumPool 中有空闲线程正在执行 poll 操作,且主线程的 offer 与空闲线程的 poll 配对成功时,主线程将把任务交给空闲线程执行,此时视作 execute() 方法执行完成;否则,将执行下面的步骤。

当初始 maximum 为空,或 maximumPool 中没有空闲线程时,将没有线程执行 poll 操作。此时,CachedThreadPool 会创建新线程执行任务,execute() 方法执行完成。


如何拟定线程池的大小?


上下文切换

多线程变编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用。为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是,当前任务在执行完 CPU 时间片切换到另一个任务之前,会先保存自己的状态,以便下次再切换回这个任务时,可以直接加载到上次的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有许多,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。


简单的拟定判断

CPU 密集型任务(N+1):

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N):

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。





相关文章
|
15天前
|
存储 前端开发 Java
【JAVA】Java 项目实战之 Java Web 在线商城项目开发实战指南
本文介绍基于Java Web的在线商城技术方案与实现,涵盖三层架构设计、MySQL数据库建模及核心功能开发。通过Spring MVC + MyBatis + Thymeleaf实现商品展示、购物车等模块,提供完整代码示例,助力掌握Java Web项目实战技能。(238字)
136 0
|
2月前
|
Java 关系型数据库 数据库
Java 项目实战教程从基础到进阶实战案例分析详解
本文介绍了多个Java项目实战案例,涵盖企业级管理系统、电商平台、在线书店及新手小项目,结合Spring Boot、Spring Cloud、MyBatis等主流技术,通过实际应用场景帮助开发者掌握Java项目开发的核心技能,适合从基础到进阶的学习与实践。
223 3
|
2月前
|
缓存 前端开发 Java
基于最新 Java 技术栈的在线任务管理系统开发实战详解
本项目基于最新Java技术栈开发在线任务管理系统,涵盖任务创建、分配、跟踪、统计等功能。采用Spring Boot 3.2.x、React 18、PostgreSQL 16等主流技术,详解项目架构设计、核心功能实现及部署流程,助力掌握现代Java全栈开发技能。
133 6
|
2月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
410 1
|
11天前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
285 100
|
2月前
|
消息中间件 Java Kafka
Java 事件驱动架构设计实战与 Kafka 生态系统组件实操全流程指南
本指南详解Java事件驱动架构与Kafka生态实操,涵盖环境搭建、事件模型定义、生产者与消费者实现、事件测试及高级特性,助你快速构建高可扩展分布式系统。
162 8
|
2月前
|
数据采集 JSON Java
Java爬虫获取1688店铺所有商品接口数据实战指南
本文介绍如何使用Java爬虫技术高效获取1688店铺商品信息,涵盖环境搭建、API调用、签名生成及数据抓取全流程,并附完整代码示例,助力市场分析与选品决策。
|
2月前
|
消息中间件 Java 数据库
Java 基于 DDD 分层架构实战从基础到精通最新实操全流程指南
本文详解基于Java的领域驱动设计(DDD)分层架构实战,结合Spring Boot 3.x、Spring Data JPA 3.x等最新技术栈,通过电商订单系统案例展示如何构建清晰、可维护的微服务架构。内容涵盖项目结构设计、各层实现细节及关键技术点,助力开发者掌握DDD在复杂业务系统中的应用。
327 0
|
3月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
90 0
|
8天前
|
人工智能 Java API
Java与大模型集成实战:构建智能Java应用的新范式
随着大型语言模型(LLM)的API化,将其强大的自然语言处理能力集成到现有Java应用中已成为提升应用智能水平的关键路径。本文旨在为Java开发者提供一份实用的集成指南。我们将深入探讨如何使用Spring Boot 3框架,通过HTTP客户端与OpenAI GPT(或兼容API)进行高效、安全的交互。内容涵盖项目依赖配置、异步非阻塞的API调用、请求与响应的结构化处理、异常管理以及一些面向生产环境的最佳实践,并附带完整的代码示例,助您快速将AI能力融入Java生态。
124 12