首页 > 编程语言 >【Java并发】线程池的原理和使用

【Java并发】线程池的原理和使用

时间:2024-12-28 11:55:31浏览次数:8  
标签:Java java 队列 创建 并发 任务 线程 import

目录

什么是线程池

为什么要用线程池(线程池的优势)

创建线程池(三大方法)

方法一:newFixedThreadPool(int)

案例

特点

方法二:newsSingleThreadExecutor()

案例

特点

方法三:newCachedThreadPool()

案例

特点

自定义线程池(七大参数)

1. corePoolSize(核心线程数)

2. maximumPoolSize(最大线程数)

3. keepAliveTime(非核心线程空闲存活时间)

4. unit(存活时间单位)

5. workQueue(工作队列) 

6. threadFactory(线程工厂)

7. handler(拒绝策略) 

拒绝策略详解(四大拒绝策略)

1. AbortPolicy(中止策略)

2. CallerRunsPolicy(调用者运行策略)

3. DiscardPolicy(丢弃策略)

4. DiscardOldestPolicy(丢弃最老策略)

线程池底层工作原理

1. 核心线程处理

2. 工作队列处理

3. 最大线程数检查

4. 创建非核心线程

5. 触发拒绝策略

如何设置线程池的线程数

CPU密集型任务

I/O 密集型任务


什么是线程池

我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。 但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程 -> 执行任务->销毁线程,会造成很大的性能开销。

那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。 这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接 获取,避免多次重复创建、销毁带来的开销。

为什么要用线程池(线程池的优势)

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

  • 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行,并且控制最大并发数量。

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

创建线程池(三大方法)

三个基本方法均是通过 Executor 框架实现的。

方法一:newFixedThreadPool(int)

这个方法创建了一个拥有固定数量线程的线程池。这里的int参数指定了池中线程的数量。如果任何一个线程因为执行任务时抛出异常而结束,那么线程池会补充一个新的线程来替换它。

ExecutorService executor = Executors.newFixedThreadPool(10);
案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 池子大小为5
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "线程");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
}

这段代码创建了一个包含5个线程的固定大小线程池,并模拟了10个线程请求服务。由于线程池中只有5个线程,所以任何时候最多只有5个线程可以同时被服务。其他的线程将会排队等待,直到有线程变得可用。当所有任务执行完毕后,代码会关闭线程池以释放资源。 

特点
  • 核心线程数等于最大线程数。

  • 工作队列是一个无界队列(LinkedBlockingQueue),这意味着如果任务提交速度超过处理速度,队列会无限增长,可能会导致内存溢出。

方法二:newsSingleThreadExecutor()

这个方法创建了一个只有一个线程的线程池,即单线程执行器。所有提交的任务都会按照顺序依次执行。

ExecutorService executor = Executors.newSingleThreadExecutor();
案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 有且只有一个固定的线程
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "线程");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
}

这段代码创建了一个单线程的线程池,并模拟了10个线程请求服务。由于线程池中只有一个线程,所以任务会按照它们提交的顺序一个接一个地执行。当所有任务执行完毕后,代码会关闭线程池以释放资源。 

特点
  • 只有一个核心线程,没有非核心线程。

  • 工作队列是一个无界队列(LinkedBlockingQueue)。

  • 适用于需要保证任务顺序执行的场景。

方法三:newCachedThreadPool()

这个方法创建了一个可根据需要创建新线程的线程池,对于短生命周期的异步任务非常合适。如果线程空闲超过60秒,则会被回收。

ExecutorService executor = Executors.newCachedThreadPool();
案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 一池N线程,可扩容伸缩
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 1; i <= 10; i++) {
                // 模拟延时看效果
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "线程");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
}

这段代码创建了一个可伸缩的线程池,并模拟了10个线程请求服务。由于线程池会根据需要创建新线程,所以理论上可以处理任意数量的任务。

特点
  • 核心线程数为0,最大线程数为Integer.MAX_VALUE

  • 工作队列是一个无界队列(SynchronousQueue),这意味着如果当前没有可用线程,则会创建一个新线程,如果创建新线程失败,则任务会等待。

  • 适用于短生命周期的异步任务,或者负载较轻的服务器。

注意:实际开发中不允许使用 Executor 去创建线程池,因为无论通过哪个方法创建都有OOM风险,而是通过 ThreadPoolExecutor 自定义创建线程池。

自定义线程池(七大参数)

Executor 提供的方法都是调用的 ThreadPoolExecutor,在实际开发中 Executor 提供的方法都有OOM的可能,所以我们一般直接使用 ThreadPoolExecutor 创建线程池,这就涉及到ThreadPoolExecutor 的七个重要参数。

ThreadPoolExecutor 源码如下:

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutor extends AbstractExecutorService {

    private final int corePoolSize;
    private final int maximumPoolSize;
    private final long keepAliveTime;
    private final TimeUnit unit;
    private final BlockingQueue<Runnable> workQueue;
    private final ThreadFactory threadFactory;
    private final RejectedExecutionHandler handler;

    private final Object acc;

    public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
            null : AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

    // ... 其他方法 ...
}

1. corePoolSize(核心线程数)

线程池中始终保持的线程数量。当有新任务来时,线程池会优先使用核心线程来执行任务,如果核心线程忙,则任务进入工作队列等待。

2. maximumPoolSize(最大线程数)

线程池中允许的最大线程数量。当工作队列满了之后,如果还有新任务到来,线程池会尝试创建新的线程来处理任务,直到达到最大线程数。

3. keepAliveTime(非核心线程空闲存活时间)

当线程池中正在运行的线程数量超过corePoolSize时,多余的空闲线程能够保持多久会被终止。用于控制线程池中的非核心线程的生命周期,以减少资源消耗。

4. unit(存活时间单位)

keepAliveTime参数的时间单位。指定keepAliveTime的时间单位,如秒、毫秒等。

TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒

5. workQueue(工作队列) 

用于存放待执行任务的阻塞队列。当所有核心线程都在忙碌时,新任务会被放入工作队列中等待执行。

6. threadFactory(线程工厂)

用于创建新线程的工厂。通过线程工厂可以自定义新线程的创建过程,例如设置线程的名称、优先级、是否为守护线程等。

7. handler(拒绝策略) 

当任务太多,无法被线程池及时处理时,采取的策略。常见拒绝策略有:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

拒绝策略详解(四大拒绝策略)

1. AbortPolicy(中止策略)

直接抛出RejectedExecutionException异常,阻止系统正常运行。适用于必须对任务提交失败做出响应的场景,比如记录日志或者向调用者反馈错误信息。

2. CallerRunsPolicy(调用者运行策略)

将任务回退给调用线程(提交任务的线程)来执行,即由提交任务的线程自己来运行这个任务。适用于提交任务的线程本身是空闲的,或者任务执行时间非常短,不希望任务被丢弃,也不希望创建新线程的场景。

3. DiscardPolicy(丢弃策略)

默地丢弃无法处理的任务,不抛出异常,也不记录日志。适用于任务丢失不会对系统造成影响的场景,或者在任务提交速度非常快,而线程池处理速度跟不上的情况下。

4. DiscardOldestPolicy(丢弃最老策略)

丢弃工作队列(阻塞队列)中最旧的任务(即等待时间最长的任务),然后尝试为新任务创建一个新线程(如果线程池尚未达到最大线程数)。适用于希望尽可能处理新任务,而不是旧任务的场景,这可以避免新任务长时间等待。

线程池底层工作原理

一张图概括线程池的底层执行流程:

1. 核心线程处理

如果核心线程数没有达到corePoolSize,线程池会创建一个新的线程来执行这个任务,即使当前有空闲的核心线程。

2. 工作队列处理

如果核心线程数已经达到corePoolSize,任务会被放入工作队列(workQueue)中等待执行。此时,如果工作队列未满,任务将在队列中等待空闲的核心线程来执行。

3. 最大线程数检查

如果工作队列已满,线程池会检查当前的线程数是否已经达到maximumPoolSize所指定的最大值。

4. 创建非核心线程

如果当前线程数没有达到maximumPoolSize,线程池会创建一个新的非核心线程来执行任务。

5. 触发拒绝策略

如果当前线程数已经达到maximumPoolSize且工作队列已满,线程池将触发拒绝策略。拒绝策略定义了如何处理无法被线程池接受的任务,例如通过抛出异常、调用者运行、丢弃任务或者丢弃队列中最旧的任务等。

如何设置线程池的线程数

任务可以分为 CPU 密集型和 I/O 密集型。

CPU密集型任务

CPU密集型任务线程数=CPU核心数+1

CPU密集型任务,比如单纯的数学计算任务,它不会涉及I/O操作,也就是说它可以充分利用CPU资源(如果涉及 I/O,在进行 I/O 的时候CPU是空闲的),不会因为I/O操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。

I/O 密集型任务

I/O 密集型任务线程数 = CPU核心数 * 2

I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用CPU的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的CPU利用率。

标签:Java,java,队列,创建,并发,任务,线程,import
From: https://blog.csdn.net/hrh1234h/article/details/144753733

相关文章

  • 探索 Java 中的 HashMap
    在Java的开发中,HashMap 是一个非常常用且重要的数据结构。它实现了 Map接口,以键值对(key-valuepair)的形式存储数据,并通过 哈希表(HashTable) 来实现高效的存储和查找。本文将深入探讨 HashMap 的核心功能、使用场景、底层原理和注意事项。1.什么是HashMap?......
  • [Java SE] 核心源码精讲:java.net.URLConnection
    概述:URLConnectionURLConnection是一个抽象类,表示指向URL【指定资源】的活动连接URLConnection可以检查服务器发送的首部,并相应地做出响应。它可以设置客户端请求中使用的首部字段。URLConnection可以用POST、PUT和其他HTTP请求方法向服务器发回数据;URLConnection类是J......
  • java中各种字符编码通过字节向16进制的互转:UTF8|GBK|unicode 字符串<=>字节<=>16进制字符
    文章目录引言I16进制、字节、编码字符之间的转换前提16进制格式字符串‌16进制格式字符串的应用场景转换原理转换流程:字符串<=>字节<=>16进制java中编码的转换APIII其他例子TCP协议字段编码基于netty实现TCP的编码设置将16进制字符串转换为字符串......
  • 基于Java的博物馆数字化网络平台
    计算机毕业设计案例Java毕业设计案例ASP.NET毕业设计案例PHP毕业设计案例微信小程序毕业设计案例基于Java的小程序自习室预约管理系统JavaSpringboot智能膳食咨询系统【12/15/02】ThinkPHP实验教学管理系统–2024计算机毕业设计Java物业管理小程序的设计与实现基于Java的......
  • java核心基础 第六章 锁
    在现实生活中,人们对锁的期望是,它能对某些事物形成一个屏障,除了上锁者外,谁也不能碰到这些事物。比如我在大街上看见了一块金子,我立刻拿来一个透明箱子,把这块金子放在箱子里然后把这个箱子锁上。假设这个箱子和锁都是绝对坚固的,只有我能打开。这个时候走在大街上的人们无论多想得......
  • javaWeb开发
    JavaWeb开发作为软件开发领域的一个重要分支,已经历经数十年的发展,并凭借其强大的跨平台能力、丰富的生态系统以及高度的安全性,成为构建企业级应用的首选技术之一。以下是对JavaWeb开发的详细解析:一、JavaWeb开发的基本概念JavaWeb开发是指使用Java语言及其相关技术栈......
  • [VUE]CALL_AND_RETRY_LAST分配失败-JavaScript堆内存不足 errno134
    使用vscode开发项目,由于项目较大,在运行npmrundev命令后,在一定的时间范围内,对vscode中的代码进行保存后,会自动编译运行,保存几次后就报错,需要重新运行npmrundev,很耗费时间)后报错报错:CALL_AND_RETRY_LASTAllocationfailed-JavaScriptheapoutofmemory(CALL_AND_RETRY_LAS......
  • 【gopher的java学习笔记】Spring Boot Starter初探
    转到java这边后,这天需要搭一个java的webservice出来,如果是以前golang的话,那我就可以非常熟练的用gin搭建一个webservice出来,核心逻辑就是写好一些rest接口实现后再加上最为灵魂的一句://启动Gin服务器在8080端口router.Run(":8080")那来到java这边,我第一反应......
  • 科普文:Java基础系列【一文搞懂字节、位运算、及其应用】
    概叙字节(Byte)是计算机信息技术用于计量存储容量的一种计量单位‌。通常情况下,一字节等于八位(bit),即1Byte=8bit。(思考一下:为啥这么规定,单位是字节,且是8比特?)字节是计算机技术中最小的可操作存储单位,通常用于描述存储容量和传输容量。‌字节是通过网络传输信息或在硬盘或内......
  • 【计算机毕业设计选题】最新毕设选题----基于Java的游戏推荐系统的设计与实现(源码+数
    博主介绍:原计算机互联网大厂开发,十年开发经验,带领技术团队几十名,专注技术开发,计算机毕设实战导师,专注Java、Python、小程序、安卓、深度学习和算法开发研究。主要服务内容:选题定题、开题报告、任务书、程序开发、文档编写和辅导、文档降重、程序讲解、答辩辅导等,欢迎咨询~......