线程池-一个很有意思的话题

文章目录

  • 前言
    • 我感觉很合理的描述(可以尝试看看)
  • 一、思想
    • 数据库连接池:
    • HTTP连接池:
    • 对象池:
    • Session池:
  • 二、代码case有点多
      • 线程池池的种类:以及创建方式
      • 重点来喽:自定义线程池:j工作中必用
        • 核心参数
        • 代码实现
      • 可以运行的Demo:固定长度线程池
  • 三、总结
    • 放开了讲:
  • 名词解释
    • 线程:
    • 线程的抽象解释:
    • 线程池的抽象解释
    • 线程实现方式
      • 1、继承Thread类:
      • 2、实现Runnable接口:
      • 3、实现Callable接口:
      • 4、使用线程池:
      • 5、使用CompletableFuture:
      • 使用ForkJoinPool


前言

线程池:简单理解就是存储线程的一个容器:(java中的池化思想)
提取关键字:线程、容器
1、什么是线程?

线程是操作系统调度执行的最小单位,用于实现并行处理,提高应用程序的响应性和性能。在Java中,线程是实现并发编程的基本构建块。 后边会有更详细的解释:

2、什么是容器(池子)

用于管理多线程的创建、执行、调度和销毁的容器。后边会有更详细的解释

抛出一个问题:线程池的参数应该怎么赋值?真的是按照理论分析,什么CPU密集型或者是IO密集型吗?

我感觉很合理的描述(可以尝试看看)

背景:

在Java多线程编程中,线程是一种重量级的系统资源,其创建和销毁都涉及显著的开销。当一个Java应用需要频繁地处理多个异步任务或并发作业时,如果每个任务都通过创建新线程来执行,系统就会花费大量时间和资源去管理这些线程。此外,资源有限的系统在面对大量线程同时运行时可能会遭遇性能瓶颈,甚至可能引起程序崩溃。

冲突:

系统资源是有限的,而无限制地创建线程会导致以下问题:
1、高昂的资源消耗,因为创建和销毁线程代价昂贵;
2、降低性能,因为过多的线程会竞争CPU和内存资源;
3、可能导致系统无法响应,如果线程数量过多超过了系统承载极限。

疑问:

在有大量短生命周期的任务需要并发处理的场景下,如何有效地管理线程,避免上述冲突,同时提高程序的响应速度和资源利用率?

答案:

线程池是解决这个问题的有效工具。它事先创建一定数量的线程并放入池中,这些线程可以循环利用,无需每次任务到来时都创建新的线程。
线程池还能通过队列管理执行前的任务,提供了调度策略来优化任务执行顺序,同时可以限制并发线程的数量,避免了系统资源耗尽的风险。
这不仅能大幅降低资源消耗,还能增强应用程序的稳定性和响应速度。此外,Java的ThreadPoolExecutor提供了丰富的配置参数,供开发者根据具体应用需求调整,这使得线程池成为管理线程的首选方案。

一、思想

池化思想:想想有没有用到过,比如数据库连接池

数据库连接池:

数据库连接是一种昂贵的资源,在前后端交互的过程中,频繁地建立和关闭数据库连接会消耗大量的系统资源和时间。
因此,使用数据库连接池来管理和复用数据库连接是常见的优化手段。
连接池维护了一组数据库连接,这些连接可以在多个客户端请求之间共享和重用。
当一个客户端请求需要执行数据库操作时,它可以从连接池中借用一个连接,使用完毕后再将其归还到连接池。
这大大减少了频繁建立和关闭连接的开销。

HTTP连接池:

当系统需要与外部服务进行HTTP通信时,也会应用池化的思想。
创建HTTP连接同样是一个昂贵的操作,特别是当涉及到SSL/TLS握手时。
因此,可以通过HTTP连接池来管理和复用HTTP连接。在Java中,例如使用Apache HttpClient时,就可以配置一个管理HTTP连接的池子。

对象池:

除了数据库连接和线程,还可以对其他重量级对象使用池化技术,例如session对象、缓冲区Buffer等。

Session池:

在Web应用程序中,管理用户会话(session)也可以采用池化的思想,通过重用session对象减少创建和销毁的次数,尤其在高并发场景下,这样的优化可以提升性能。

池化资源管理的目标是提高资源的利用率,减少创建和销毁资源的开销,同时保证系统的稳定性和响应速度。为了实现这一点,池中的资源通常有最小和最大数量的限制,以及资源的生命周期管理策略(如超时,健康检查等)。在设计系统的架构时,应当综合考虑系统的实际需求和资源管理的最佳实践。

二、代码case有点多

线程池池的种类:以及创建方式

在Java中,有许多种线程池,主要通过java.util.concurrent包中的Executors类来创建。这些线程池都有各自的特点和用途。以下是主要的几种线程池的创建方式和它们的参数:

1、固定大小的线程池 (newFixedThreadPool):

int nThreads = Runtime.getRuntime().availableProcessors();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(nThreads);
参数:
nThreads: 池中允许的活动线程数。当所有线程都处于活动状态时,新任务会在队列中等待,直到有线程可用。

2、单线程化的线程池 (newSingleThreadExecutor):

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
参数:无,线程池的大小固定为1。这保证了所有任务都是顺序执行,在同一个线程中。

3、可缓存的线程池 (newCachedThreadPool):

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
参数:无。初始化时线程池没有固定的大小,线程可以根据需要创建,但是如果线程超过60秒没有被使用就会被回收。


4、计划任务线程池 (newScheduledThreadPool):

int corePoolSize = Runtime.getRuntime().availableProcessors();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(corePoolSize);
参数:

corePoolSize: 核心线程数,即将保留在池中的线程数,即使它们处于空闲状态。

5、单线程化的计划任务线程池 (newSingleThreadScheduledExecutor):

ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
参数:无,类似于单线程化的线程池,但它可以安排任务在将来执行,或者周期性地执行。


6、工作密取线程池 (newWorkStealingPool):

ExecutorService workStealingPool = Executors.newWorkStealingPool();
参数:默认情况下,核心线程数被设置为Java虚拟机(JVM)可用的处理器数量。这个线程池类型是基于Fork-Join框架,适用于并行运行任务,内部使用了工作窃取算法。

7、代码完整示例:

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

public class MultipleThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);

        // 创建单线程化的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        // 创建可缓存的线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        // 创建计划任务线程池
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);

        // 创建单线程化的计划任务线程池
        ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

        // 创建工作密取线程池
        ExecutorService workStealingPool = Executors.newWorkStealingPool();

        // 一个简单的任务
        Runnable task = () -> {
            System.out.println("Task executed by: " + Thread.currentThread().getName());
        };

        // 向各种线程池提交任务
        fixedThreadPool.submit(task);
        singleThreadExecutor.submit(task);
        cachedThreadPool.submit(task);
        scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS);
        singleThreadScheduledExecutor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
        workStealingPool.submit(task);

        // 关闭线程池
        fixedThreadPool.shutdown();
        singleThreadExecutor.shutdown();
        cachedThreadPool.shutdown();
        scheduledThreadPool.shutdown();
        singleThreadScheduledExecutor.shutdown();
        workStealingPool.shutdown();
    }
}

重点来喽:自定义线程池:j工作中必用

核心参数

corePoolSize:核心线程数 - 线程池中始终运行的线程数,即使它们处于空闲状态也不会被回收。
maximumPoolSize:最大线程数 - 线程池中允许存在的最大线程数。
keepAliveTime:存活时间 - 当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
unit:存活时间的单位 - keepAliveTime参数的时间单位。
workQueue:阻塞队列 - 用于存储等待执行的任务的阻塞队列。
threadFactory:线程工厂 - 用于创建新线程的线程工厂。
handler:饱和策略 - 当阻塞队列满而且线程数达到最大值时,新提交的任务如何处理的饱和策略。

代码实现
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ThreadFactory;

public class CustomThreadPoolExample {

    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 2;
        // 最大线程数
        int maxPoolSize = 4;
        // 空闲线程等待新任务的最长时间
        long keepAliveTime = 10;
        // 时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        // 任务队列
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
        // 线程工厂(可选)
        ThreadFactory threadFactory = new CustomThreadFactory();
        // 拒绝策略(当任务太多执行不过来时使用,如CallerRunsPolicy/AbortPolicy/DiscardPolicy/DiscardOldestPolicy)
        ThreadPoolExecutor.AbortPolicy handler = new ThreadPoolExecutor.AbortPolicy();

        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler);

        // 提交任务给线程池执行
        threadPoolExecutor.submit(() -> System.out.println("Task 1 executed by: " + Thread.currentThread().getName()));
        threadPoolExecutor.submit(() -> System.out.println("Task 2 executed by: " + Thread.currentThread().getName()));

        // 关闭线程池(平稳地关闭)
        threadPoolExecutor.shutdown();
    }

    // 自定义线程工厂
    static class CustomThreadFactory implements ThreadFactory {
        private final AtomicInteger threadNumber = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "CustomThread-" + threadNumber.getAndIncrement());
            System.out.println("Created " + t.getName());
            return t;
        }
    }
}


这个例子中:

LinkedBlockingQueue被用作任务队列,其容量设为2。这意味着,除了核心线程外,最多有2个任务可以在队列中等待。
CustomThreadFactory是一个自定义的线程工厂类,用于自定义创建线程的方式(例如自定义线程名)。
当工作队列满了且有超过maxPoolSize个线程运行时,AbortPolicy会导致新提交的任务被丢弃并抛出RejectedExecutionException。
自定义线程池允许你精细控制多线程环境中任务的执行,而这通常是必要的,因为默认提供的线程池很可能不符合你的特定性能要求。在设计自定义线程池时,应当对上述参数有透彻理解,以避免资源耗尽和性能瓶颈。

可以运行的Demo:固定长度线程池

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        int numThreads = 5;
        ExecutorService executorService = Executors.newFixedThreadPool(numThreads);

        // 提交一些任务给线程池执行
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executorService.submit(() -> {
                // 这里是所有任务都会执行的代码
                System.out.println("Task " + taskId + " is running on: " + Thread.currentThread().getName());
                // 模拟任务执行时间
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池,不再接受新的任务,正在运行的任务会执行完
        executorService.shutdown();
        
        try {
            // 等待所有任务完成,或者超时,或者当前线程被中断
            // 在这个例子中最多等待10秒
            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                // 如果线程池里的任务没能在10秒内完成,强制关闭所有正在执行的任务
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
        
        System.out.println("Finished all threads");
    }
}
补充:
注解说明:

创建线程池:使用Executors.newFixedThreadPool(numThreads)创建一个固定大小的线程池,
numThreads是你想要在池中同时运行的最大线程数。
提交任务:循环创建任务,通过executorService.submit()方法将任务提交给线程池执行。在这里,
每个任务都是一个打印ID和当前线程名的Lambda表达式。
关闭线程池:调用executorService.shutdown()以停止接收新任务,但已提交的任务会继续执行直至完成。
等待线程池关闭:通过调用executorService.awaitTermination()等待所有任务执行完成,
或者在提供的时间结束后,如果有未完成的任务,则通过executorService.shutdownNow()尝试立即停止所有线程的执行。
注意,尽量避免使用Executors类的静态工厂方法创建无界(unbounded)线程池,如
Executors.newCachedThreadPool(),因为它们会根据需要创建新线程,但没有上限,
可能导致创建过多的线程而耗尽系统资源。在实际应用中,
根据任务特性和系统性能需求自定义线程池或使用有界设置会更好。

三、总结

是不是被问过:线程池中的各个参数数你们是怎么设定的?
八股回答:
线程数的设置主要取决于机器CPU核数,机器内存以及IO支持的最大QPS还有任务类型?
CPU密集型:核心线程数=机器核数+1
IO密集型:核心线程数=2*机器核数
反正我之前就是这么回答的,但是现在看好像工作中也是这么用的,最起码没有出过问题?
但是实际上,这样是不合理的,那么为啥不合理?
首先这些都是理论依据,并不是业务的真实场景
其次和环境等诸多因素影响

放开了讲:

corePoolSize(核心线程数):
这是线程池中始终保持运行的线程的最小数量。当提交一个任务时,如果运行的线程少于corePoolSize,则创建一个新线程来处理请求,即使有空闲线程也是如此。
设定原则:
如果你的应用经常有很多短生命周期的任务,可以设定一个相对较高的核心线程数以减少线程创建的开销。
对于长生命周期或CPU密集型的任务,核心线程数应不超过处理器数量,这样可以避免CPU过载。

maximumPoolSize(最大线程数):
线程池允许的最大线程数。如果队列满了,并且已创建的线程数小于maximumPoolSize,线程池会再创建新线程来处理任务。
设定原则
maximumPoolSize的设置应该基于你的资源限制(如内存、CPU)和应用程序的需求。一个过大的值可能会消耗过多的资源,而一个过小的值可能会导致任务等待时间过长。

keepAliveTime(空闲线程存活时间):
当线程数超过核心线程数时,这些多余的线程能保留多长时间。在这个时间之后,如果线程不是通过执行任务要保持忙碌状态,就会被终止。
设定原则
对于需要快速释放资源的应用程序,你可以设置一个较短的存活时间。
对于不希望频繁创建线程的应用程序,应设置较长的存活时间。

workQueue(工作队列):
用来存储等待执行的任务的阻塞队列。
设定原则
如果你需要平滑系统负载,则应选择有界队列,如ArrayBlockingQueue。
如果你希望线程池能够处理一个无限数量的任务,可以使用无界队列,如LinkedBlockingQueue。

threadFactory(线程工厂):
用于创建新线程的工厂。
设定原则:
如果你想要自定义线程的创建方式,可以提供一个ThreadFactory。例如,你可以设置自定义的线程名、设置守护线程或设置优先级。

handler(饱和策略):
当队列和最大线程池数都满了后,拒绝策略定义了线程池如何处理新提交的任务。
设定原则
选择一个能反映你的需求的饱和策略,比如:ThreadPoolExecutor.CallerRunsPolicy、ThreadPoolExecutor.AbortPolicy、ThreadPoolExecutor.DiscardPolicy 或 ThreadPoolExecutor.DiscardOldestPolicy。

当你根据这些原则设计自定义线程池时,可能需要考虑以下几个问题
系统的资源限制是多少?
任务的特性是什么?CPU密集型、IO密集型或两者混合?
任务执行的平均时间长度是多少?
系统并发的需求是什么?期望吞吐量和相应时间是多少?
由于这些参数密切关联,通常需要多次尝试和调整来找出最适合你应用程序的配置。不过,始终要记住,避免创建过多的线程,因为它们会消耗系统资源(如CPU和内存),并且大量的上下文切换会降低应用程序的整体性能。

是不是感觉考虑挺多
其实在实际中,若是做到精细化,还得进行压测,来进行调整,但是若是有时候线上系统并发超过我们的压测能力,那么怎么办,所以这里现在好多大公司都会引入一个新架构来实现:就是对线程池的核心参数的监控
hippo4j(河马)
https://hippo4j.cn/zh/docs/user_docs/intro
线程池-一个很有意思的话题_第1张图片
好了,去学习吧,然后和面试官吹牛逼就行了,这才是你和别人拉开差距的点

名词解释

线程:

线程是操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。在单个程序中,线程运行在共享的内存空间内,这意味着它们可以共享程序的数据和资源。不同的线程可以并行地执行不同的任务,通常被用于执行耗时操作,而不会冻结整个程序的运行。

线程是进程中的一个独立的顺序控制流程。
一个进程中可以并发多个线程,每个线程并行执行不同的任务。在多核处理器系统中,多个线程甚至可以实现真正的并行计算,因为不同的线程可以在不同的处理器核上同时运行。

从编程的角度来看,线程是一段能够被系统调度执行的代码。
在Java中,可以通过实现Runnable接口或者继承Thread类来创建线程,并通过调用线程的start()方法来启动线程。线程的创建和调度由Java虚拟机(JVM)和操作系统共同管理。(总共五种实现方式,后边会描述)

总结
线程是操作系统调度执行的最小单位,用于实现并行处理,提高应用程序的响应性和性能。在Java中,线程是实现并发编程的基本构建块。

线程的抽象解释:

把线程想象成餐厅里的厨师。你知道,当你去餐厅吃饭时,你的订单(比如说一道菜)需要一个厨师来烹饪。在计算机里,一个程序可以看作是整个餐厅,而线程就像是餐厅里工作的厨师。
如果这个餐厅(程序)里只有一个厨师(单线程),那么无论多少人下订单,这个厨师得一次处理一个菜单。他得先做完一道菜,然后再做下一道,顾客可能就要等很久。
现在,如果餐厅里有多个厨师(多线程),每一个厨师可以同时处理一个订单。这样,许多顾客的订单可以同时被烹饪,厨师们工作得效率高,顾客的等待时间就缩短了。
在计算机中,线程的作用就像这些厨师一样。线程可以让计算机同时进行多个任务,比如同时运行多个程序,或者在一个程序中同时执行多项任务(比如在一个文本编辑器中,一个线程负责显示文本,另一个线程负责检查拼写错误)。
所以,线程就是允许计算机同时做多个事情的工人。这样多个线程工作,可以让程序更加高效,响应更快,给用户带来更好的体验。

线程池的抽象解释

线程池就像一个工作队列与工人的结合。想象这样一个场景:你负责一个工厂,有很多任务需要完成,而完成这些任务需要工人。你可以为每个任务雇佣一个工人,让他完成任务后再解雇,这样显然是低效且成本很高的。相反,你可以招聘一定数量的工人,组建一个团队,并将任务分配给这个团队,工人完成一个任务后不用离开,而是待命等待下一个任务。线程池正是这样一个"工人团队"

线程实现方式

1、继承Thread类:

可以通过继承java.lang.Thread类来创建一个线程。这里,需要重写Thread类的run()方法,该方法将包含将在新线程中执行的代码。一旦线程类的对象被创建,并调用了它的start()方法,线程就会被启动,并且执行run()方法中定义的操作。
优点:编写简单,直接调用start()方法来启动线程。
缺点:Java不支持多继承,如果你的类已经继承了另一个类,就不能再继承Thread类。
另外,继承Thread类会导致线程类和任务代码紧密耦合。

2、实现Runnable接口:

另一种创建线程的方式是实现java.lang.Runnable接口。
与Thread类相似,你必须实现Runnable接口的run()方法。然而,因为Runnable只是一个接口,你可以创建一个实现了Runnable接口的类的实例,然后将它作为参数传递给Thread类的构造器,以此来创建一个线程。

优点:更加灵活,因为Runnable是一个接口,你可以实现Runnable,同时也可以继承另一个类。此外,它也推广了代码的复用和分离职责,使得线程管理和业务逻辑分开。
缺点:不能直接使用线程类(如Thread)的一些特定方法。
Java SE5之后,随着java.util.concurrent包的引入,还提供了更高级的线程实现方式:

3、实现Callable接口:

java.util.concurrent.Callable是一个接口,它类似于Runnable接口,但它可以返回一个结果,并且能够抛出异常。你需要实现call()方法而不是run()方法。Callable通常与FutureTask或线程池ExecutorService一起使用,这样你可以获得线程执行结束后的结果。

优点:可以返回值,可以抛出异常。
缺点:相较于直接使用线程,使用起来更复杂,通常需要配合Future和线程池。

4、使用线程池:

通过java.util.concurrent.Executors类提供的静态工厂方法,可以创建不同类型的线程池。使用线程池可以有效地管理线程生命周期,提供更复杂的功能,比如线程调度、线程复用等。

优点:提供了强大的线程管理能力,是构建多线程应用程序的首选方式。可以减少在创建线程和销毁线程上所花的时间和资源,可以很好地控制最大并发线程数,提高系统资源的使用率。
缺点:适应性强,需要更多的学习和理解,不适合对线程管理要求不高的简单任务。
Java的线程模型是建立在操作系统线程模型之上的,并通过Java虚拟机(JVM)来抽象线程的实现和管理。这些实现方式提供强大的机制用于创建、管理以及调度线程,使得开发多线程应用变得更加方便和高效。

5、使用CompletableFuture:

CompletableFuture是在Java 8中引入的一种新的Future类型,它提供了更多的方法和灵活性。与传统的Future不同的是,CompletableFuture可以编程式地完成,它任务的执行可能是由手动完成,或者完成其它异步任务的结果。它还允许你将这些Future组合成链式调用,或并发地运行多个Future,最后将它们的结果组合起来。
优点:提供了非阻塞的异步编程模型,支持Lambda表达式和函数式编程,允许多个依赖阶段,易于编写复杂的异步流水线和触发多阶段的任务执行。
缺点:API相对复杂,使用不当易造成难以调试的问题。
CompletableFuture提供了方法来显式地完成计算、处理异常、组合多个Future、响应Future的完成事件等。

使用ForkJoinPool

Java 7引入了ForkJoinPool来支持大任务的分解成小任务并行计算,然后合并结果的编程模式。通常情况下,你会创建一个继承自RecursiveAction或RecursiveTask(如果要有返回结果)的类,并将这个任务提交到ForkJoinPool实例。

以上整理了Java中线程实现的主要方式,包括传统方式和并发包下的高级工具,以及在Java 8中新增的异步编程模式。这些机制让Java在多线程和并发处理上有了非常强大和灵活的能力。
但Java随着时间的推移和更新,增加了很多处理并发任务的工具类和框架,为多线程编程提供了丰富和灵活的选择。上述就是目前Java中创建和管理线程的主要方式。不过,具体项目中选择哪一种方式取决于具体需求,任务的性质以及预期的吞吐量等因素

你可能感兴趣的:(java)