Java线程池的学习及自定义线程池实现复用

线程池简介

线程池:事先创建若干空闲的线程放入一个池中(容器),当一个任务提交到线程池时,线程池就会启动一个空闲的线程去处理任务,当任务结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。

实际开发项目中禁止自己 new 线程。 必须使用线程池来维护和创建线程。

一、使用线程池的好处?

核心点:复用机制 提前创建好固定的线程一直在运行状态, 实现复用 限制线程创建数量。

  • 实现线程复用

  • 降低资源消耗

  • 提高响应速度

  • 方便管理线程

线程池核心点:复用机制

  1. 提前创建好固定的线程一直在运行状态----死循环实现

  2. 提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

  3. 正在运行的线程就从队列中获取该任务执行

二、通过Executors创建线程池?(4种)

线程池的创建方式可以通过Executors创建,但是不推荐使用自带的线程池,因为底层都是都是基于 ThreadPoolExecutor封装的,而ThreadPoolExecutor构造函数中都是封装的一个无界的队列缓存。 有可能会出现线程池溢出问题(OOM)。

  1. Executors.newCachedThreadPool()        可缓存线程池

  2. Executors.newFixedThreadPool()           可定长度 限制最大线程数

  3. Executors.newScheduledThreadPool()  可定时

  4. Executors.newSingleThreadExecutor()  单例

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
​
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
​
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
Test:
public static void main(String[] args) {
    //1. 单个线程
    ExecutorService pools = Executors.newSingleThreadExecutor();
    //2. 可缓存线程池 ==》 可伸缩的,遇强则强,遇弱则弱
    ExecutorService pools1 = Executors.newCachedThreadPool();
    //3. 固定数量线程数
    ExecutorService pools2 = Executors.newFixedThreadPool(5);
    //4. 可定时
    ScheduledExecutorService pools3 = Executors.newScheduledThreadPool(2);
​
    try {
        for (int i = 0; i < 66; i++) {
            pools1.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        pools1.shutdown();
    }
}

三、线程池的七个核心参数?(重点)

1. corePoolSize(核心工作线程数)

核心线程数量 一直正在保持运行的线程。

2. maximumPoolSize(最大核心线程数)

线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于最大线程数,则线程池会创建新的线程来执行任务。

最大线程数如何设置?

1.CPU密集型: 你的电脑是几核,就设定为几,可以保证CPU效率最高  ==》Runtime.getRuntime().availableProcessors()
2.IO密集型: 大于 你程序中十分耗资源IO的线程

3. keepAliveTime(多余线程存活时间)

超出 corePoolSize 后创建的线程的存活时间。

4. TimeUnit(时间单位)

keepAliveTime的时间单位。

5. workQueue(队列)

用于传输和保存等待执行任务的阻塞队列。

线程池的阻塞队列?

阻塞队列

(1)LinkedBlockingQueue:对应的线程池:newSingleThreadExecutor( )和newFixedThreadPool(int n)

LinkedBlockingQueue,它的容量是 Integer.MAX_VALUE,为 231 -1 ,是一个非常大的值,可以认为是无界队列。

FixedThreadPool 和 SingleThreadExecutor 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。

(2)SynchronousQueue:对应的线程池是 newCachedThreadPool( )

线程池 CachedThreadPool 的最大线程数是 Integer.MAX_VALUE,可以理解为线程数是可以无限扩展的。

CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

(3)DelayedWorkQueue:对应的线程池分别是 newScheduledThreadPool (int n)和newSingleThreadScheduledExecutor( )

这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。

DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构(堆的应用之一就是 优先级队列)。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

线程池队列满了,任务会丢失吗?

如果队列满了,且任务总数>最大线程数,则当前线程走拒绝策略。 可以自定义异拒绝异常,将该任务缓存到 redis、本地文件、mysql 中后期项目启动实现补偿。

6. threadFactory(线程创建工厂)

线程池内部创建线程所用的工厂。 用于创建新线程。threadFactory创建的线程也是采用newThread()方式。

7. handler(拒绝策略)

当线程池和队列都满了,再加入线程会执行此策略。

线程池的五种拒绝策略?

通常有以下五种策略:
1. AbortPolicy:         丢弃任务,抛出RejectedExecutionException异常。 
2. DiscardPolicy:       丢弃任务,但是不抛出异常。忽视,什么都不会发生。 
3. DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。 
4. CallerRunsPolicy:    执行任务,哪来的去哪里
5. 实现RejectedExecutionHandler接口,可自定义处理器
​
=================线程池的默认拒绝策略为 AbortPolicy========================

核心代码: 

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;
}

四、为什么阿里巴巴不建议使用 Executors?

因为默认的 Executors 线程池底层是基于 ThreadPoolExecutor 构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生内存溢出(OOM),会导致我们最大线程数会失效。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 
​
说明:Executors 返回的线程池对象的弊端如下: 
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM 
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

五、手写一个线程池?

public class threadpool {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                Runtime.getRuntime().availableProcessors(),//获取CPU处理器是几核的
                30,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
                );
​
        try {
            for (int i = 0; i < 10; i++) {
                threadPoolExecutor.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}

六、线程池底层如何实现复用?

本质思想:创建一个线程,不会立马停止或者销毁而是一直实现复用。

  1. 提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗 cpu 的资源)

  2. 当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略;

  3. 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;

七、线程池创建的线程会一直在运行状态吗?

不会 例如:配置核心线程数 corePoolSize 为 2 、最大线程数 maximumPoolSize 为 5。

当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

我们可以通过配置超出 corePoolSize 核心线程数后创建的线程的存活时间例如为 60s, 在 60s 内没有核心线程一直没有任务执行,则会停止该线程。

八、ThreadPoolExecutor底层实现原理?线程池的工作流程?

  1. 判断核心线程数是否已满,如果没满,及时现在有空闲线程,也创建一个新的工作线程来执行任务

  2. 如果核心线程数已满,再判断任务队列是否已满,如果没满则将新提交的任务添加在工作队列中。

  3. 如果任务队列已满,则判断最大线程数是否已满,如果没满则创建一个新的工作线程来执行任务,如果最大线程数已满,则执行拒绝策略。

线程池底层 ThreadPoolExecutor 底层实现原理

  1. 当线程数 < 核心线程数时,创建线程。

  2. 当线程数 >= 核心线程数,且任务队列未满时,将任务放入任务队列。

  3. 当线程数 >= 核心线程数,且任务队列已满

  4. 若线程数 < 最大线程数,创建线程

  5. 若线程数 = 最大线程数,抛出异常,拒绝任务

实际上最多执行多少个任务: 核心线程数 + 任务队列数 + (最大线程数-核心线程数)

常见面试题目:

  1. 手写一个线程池

  2. 你们项目在哪些地方会使用到线程池?

  3. 如果让你实现具有缓存功能的线程池的类怎么实现?

  4.  ArrayBlockingQueue和LinkedBlockingQueue有什么区别?

  5. 当个流量高峰时,线程池的队列是满的,最大线程数也是满的,线程池已经达到满负荷的状态,后来流量下降了,线程池怎么去回收这些线程?

工作中自定义线程池:

(1)自定义一个线程池

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 公用线程池
 *
 * @author Y
 * @since 2024/6/19 上午10:57
 **/
@Configuration
@EnableAsync(proxyTargetClass = true)
public class ThreadPoolConfiguration {

    @Bean(name = "AsyncThreadPool")
    public ExecutorService businessExecutor() {
        // 获取可用的CPU核心数
        int cpuNum = Runtime.getRuntime().availableProcessors();

        // 定义一个线程工厂,用于创建新的线程
        ThreadFactory THREAD_FACTORY = new ThreadFactory() {
            private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            public Thread newThread(Runnable r) {
                // 使用默认工厂创建新线程,并设置线程名称
                Thread thread = this.defaultFactory.newThread(r);
                thread.setName("Async_threadPool" + this.threadNumber.getAndIncrement());
                return thread;
            }
        };

        // 创建一个线程池执行器
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                cpuNum, // 核心线程数
                cpuNum, // 最大线程数
                0, // 空闲线程的存活时间
                TimeUnit.MILLISECONDS, // 存活时间的单位
                new LinkedBlockingQueue<>(500), // 任务队列,最大容量500
                THREAD_FACTORY // 线程工厂
        );
        return threadPoolExecutor;
    }
}

自定义一个线程工厂,给每个新线程分配一个唯一的名称。可以便于打印日志的时候看到每个线程的名字。【可以去掉这段代码】

代码中使用:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值