讨伐Java多线程与高并发——线程池篇

本文是学习Java多线程与高并发知识时做的笔记。

这部分内容比较多,按照内容分为5个部分:

  1. 多线程基础篇
  2. JUC篇
  3. 同步容器和并发容器篇
  4. 线程池篇
  5. MQ篇

本篇为线程池篇。

目录

1 线程池简介

2 三种线程池

2.1 单线程化线程池

2.2 定长线程池

2.3 可缓存线程池

2.4 注意事项

3 七大参数

3.1 线程池参数

3.2 自定义线程池

4 四种拒绝策略

4.1 AbortPolicy

4.2 CallerRunsPolicy

4.3 DiscardPolicy

4.4 DiscardOldestPolicy


1 线程池简介

线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。使用线程池避免了在处理短时间任务时创建与销毁线程的代价。线程池中的线程数也不宜过多,否则会导致额外的线程切换开销,线程数一般取CPU数量+2比较合适。

使用线程池的优势:

  • 降低资源的消耗:通过重复利用已经创建好的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,不需要等待线程创建就能立刻执行。
  • 方便管理线程:对线程进行统一的分配、调优和监控。

即 线程复用、控制最大并发数、管理线程。

关于线程池必须知道的是:三种线程池、七大参数、四种拒绝策略。

2 三种线程池

java.util.concurrent包中提供了一个工具类Executors。

所谓的三种线程池是指java.util.concurrent.Executors类中定义的三种线程池:

  • 单线程化线程池
  • 定长线程池
  • 可缓存线程池

2.1 单线程化线程池

单线程化线程池中线程数为1,所有的工作任务都会由这个唯一的线程来执行。

创建方法:

ExecutorService threadPool = Executors.newSingleThreadExecutor();

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

public class Test {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> { //使用线程池来执行多线程任务
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行结果:

pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok

2.2 定长线程池

定长线程池 可以控制线程数目,以此来控制线程的最大并发数。

创建方法:

ExecutorService threadPool = Executors.newFixedThreadPool(最大线程数);

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

public class Test {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> { //使用线程池来执行多线程任务
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行结果:

pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-3 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-1 ok

2.3 可缓存线程池

可缓存线程池,由线程池自适应调整任务需要的线程数。

自适应:当线程池要开启新的线程任务时,若线程池中有空闲线程,则由空闲线程来处理任务,若没有空闲线程,则新建线程。

创建方法:

ExecutorService threadPool = Executors.newCachedThreadPool();

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

public class Test {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> { //使用线程池来执行多线程任务
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行结果:

pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-6 ok
pool-1-thread-5 ok
pool-1-thread-1 ok
pool-1-thread-5 ok

2.4 注意事项

需要注意的是:在《阿里巴巴Java开发手册中》,【强制】要求线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式(自定义线程池),这样的处理方法让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

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

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

3 七大参数

3.1 线程池参数

我们试着分析三种线程池创建方法的源码:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

发现它们都返回了一个ThreadPoolExecutor对象。

查看ThreadPoolExecutor类的构造器:

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

可以看到定义线程池的七个参数:

  • int corePoolSize,核心线程池大小。
  • int maximumPoolSize,最大线程池大小。
  • long keepAliveTime,保持活跃时间。
  • TimeUnit unit,保持活跃时间的单位。
  • BlockingQueue<Runnable> workQueue,工作(阻塞)队列。
  • ThreadFactory threadFactory,线程工厂。
  • RejectedExecutionHandler handler,拒绝执行处理程序。

接下来讲一个 银行营业厅 的模型,方便大家理解线程池中的各个参数的意义。

从前有一个银行营业厅,【线程池】

这个银行营业厅有5个窗口,【最大线程池大小=5】

平时业务清闲时,只有2个窗口办理业务。【核心线程池大小=2】

营业厅内设有候客区,候客区有3个座位。【工作(阻塞)队列,大小=3】

业务繁忙的时候,有一次,

先来了2位顾客,他们一来就到2个常开的窗口办理业务,

又来了3位顾客,他们发现所有开放的窗口都有人正在办理业务,就坐在候客区的座位上等待,

又来了1位顾客,营业厅发现开放的窗口、候客区都没有位置了,就又打开1个窗口来营业,

又来了2位顾客,营业厅的所有窗口全部开放营业,候客区坐满,

又来了1位顾客,营业厅拒绝为他提供服务。【拒绝执行处理程序】

业务繁忙的时段过去以后,顾客进来的速度不再能赶上所有开放窗口的处理速度,

有一个窗口有1个小时都没有顾客去办理业务,【保持活跃时间=1,保持活跃时间单位=小时】

这个窗口就关闭了。

在上面的模型中唯独没有提到线程工厂,它是用来创建线程的。【工厂模式】

这里使用线程工厂是为了统一在创建线程时设置一些参数,一般不需要修改。

关于 拒绝执行处理程序,我们到 四种拒绝策略 章节再进一步讲解。

3.2 自定义线程池

在了解了线程池的七大参数后,我们可以通过new ThreadPoolExecutor(,,,,,,)的方式创建自定义线程池。在《阿里巴巴Java开发手册中》,【强制】要求使用这种方式创建线程池。

创建一个自定义线程池:

import java.util.concurrent.*;

public class BusinessHall {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2, //核心线程池大小=2
                5, //最大线程池大小=5
                2, //保持活跃时间=2
                TimeUnit.SECONDS, //保持活跃时间单位=秒
                new LinkedBlockingDeque<>(3), //工作(阻塞)队列,大小=3
                Executors.defaultThreadFactory(), //默认线程工厂
                new ThreadPoolExecutor.AbortPolicy() //一种拒绝执行处理程序
        );
        try {
            for (int i = 0; i < 8; i++) { //保证没有任务被拒绝执行的最大线程数=5+3
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行结果:

pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-5 ok
pool-1-thread-2 ok

工作中创建自定义线程池,最大线程池大小设多少比较合适?

首先评估活跃进程属于CPU密集型还是IO密集型:

  • CPU密集型:程序的大多数时间花在计算上。
  • IO密集型:程序的大多数时间花在input和output上。

如果是CPU密集型程序,就将最大线程池大小设为服务器逻辑处理器的数目。

获取服务器逻辑处理器数目的API:

public class Test {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

 如果是IO密集型,评估程序中有多少条比较耗费IO资源的线程,只要比这个数目大就ok,可以设为2倍。

4 四种拒绝策略

线程池拒绝执行处理程序共有四种拒绝策略:

  • AbortPolicy,默认。
  • CallerRunsPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

4.1 AbortPolicy

当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,抛出异常:

java.util.concurrent.RejectedExecutionException

代码测试:(可能不会报错,多测几次)

import java.util.concurrent.*;

public class BusinessHall {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy() //拒绝执行处理程序
        );
        try {
            for (int i = 0; i < 10; i++) { //任务数目超过最大线程池大小于工作(阻塞)队列大小之和
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行抛出异常:

Exception in thread "main" java.util.concurrent.RejectedExecutionException

4.2 CallerRunsPolicy

当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将超出的任务返回到任务来源的线程执行。

代码测试:

import java.util.concurrent.*;

public class BusinessHall {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy() //拒绝执行处理程序
        );
        try {
            for (int i = 0; i < 10; i++) { //任务数目超过最大线程池大小于工作(阻塞)队列大小之和
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } finally {
            threadPool.shutdown(); //线程池一定要关闭
        }
    }
}

运行结果:

pool-1-thread-1 ok
pool-1-thread-3 ok
main ok
pool-1-thread-4 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok
pool-1-thread-1 ok

4.3 DiscardPolicy

当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将超出的任务舍弃。不会抛出异常。

4.4 DiscardOldestPolicy

当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将工作(阻塞)队列中最老的任务舍弃,超出的任务进入队列。不会抛出异常。

学习视频链接:

https://www.bilibili.com/video/BV1B7411L7tE

加油!(ง •_•)ง

猜你喜欢

转载自blog.csdn.net/qq_42082161/article/details/114002353
今日推荐