【Java并发编程】——线程池

一、什么是线程池?

1. 什么是线程池

线程池 顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销

例如:为线程池提供一个Runnable, 就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡,而是留着池中准备为下一个请求提供服务。

2. 为什么使用线程池

线程是一种昂贵的资源,需要非常大的开销,主要包括:

  • 线程的创建于启动的开销
  • 线程的销毁
  • 线程调度的开销
  • 线程的数量总是受限于处理器的数目

因此从整个系统的角度来看,我们需要一种有效使用线程的方式,从而减少开销。
线程池就是有效使用线程的一种常见方式

3. 怎么创建线程池

创建线程池的方式有多种,这里你只需要答 ThreadPoolExecutor 即可。

ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。

4. 线程池有什么优点?

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

二、线程池的状态

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
  • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

三、如何合理分配线程池大小?

简单来说就是要根据CPU密集和IO密集来分配
CPU密集:

  • CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
  • CPU密集任务只有在真正的多核CPU才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPu总的运算能力是有限制的。

IO密集:

  • IO密集型 ,即该任务需要大量的IO,即大量的阻塞。
  • 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待过程中,所以在IO密集型任务中使用多线程可以大大的加速程序运行,
  • 即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

分配CPU和IO密集:

  • CPU密集型时:任务可以少配置线程数,大概和机器的CPU核数相当,这样可以使得每个线程都在执行任务
  • IO密集型时:大部分线程都阻塞,故需要多配置线程数,2*CPU核数

结论:

  • 线程等待时间比CPU执行时间比例越高,需要越多线程
  • 线程CPU执行时间比等待时间比例越高,需要越少线程

四、执行原理

1. 线程池的处理流程

【Java并发编程】——线程池_第1张图片

提交一个任务到线程池中,线程池的处理流程如下:

  1. 首先判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲还有核心线程没有被创建)则创建一个新的工程线程来执行任务。否则,进入下一个流程
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里等待。否则,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。否则,则交给饱和策略来处理这个任务。

2. 一个简单的线程池Demo

【实例】
为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)

/**
 * @Author: LiangYiFeng
 * @Description 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @Date: Create in 2022/8/15 11:54
 * @Modified By:
 */
public class MyRunnable  implements Runnable{


    private  String command;

    public MyRunnable(String command) {
        this.command = command;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "Start.Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + "End.Time = " + new Date());

    }

    private void processCommand() {
        try{
            Thread.sleep(5000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return "MyRunnable{" +
                "command='" + command + '\'' +
                '}';
    }
}

编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;


    public static void main(String[] args) {
        // 使用阿里巴巴 推荐的创建线程池的方式
        // 通过ThreadPoolExecutor 构造函数自定义参数创建
        ThreadPoolExecutor executor =  new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            Runnable worker = new MyRunnable("" + i);
            executor.execute(worker);

        }

        // 终止线程池
        executor.shutdown();
        while (!executor.isTerminated()){

        }
        System.out.println("Finish all Threads");
    }
    

}

可以看到我们上面的代码指定了:

  • corePoolSize: 核心线程数为 5。
  • maximumPoolSize :最大线程数 10
  • keepAliveTime : 等待时间为 1L。
  • unit: 等待时间的单位为 TimeUnit.SECONDS。
  • workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
  • handler:饱和策略为 CallerRunsPolicy。

输出结果:

pool-1-thread-4Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:35 CST 2022
Finish all Threads

:Runnable+ThreadPoolExecutor:

五、Executor

1. 什么是 Executor 框架?

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

2. 为什么使用 Executor 集合?

每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。

所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。

3. Executor 和 Executors 的区别?

  • Executor 是一个接口,只有一个execute 方法,用于执行能执行我们的线程任务
  • Executors 是一个普通工具类,里面实现了各种新建线程池方法能按照我们的需求创建了不同的线程池,来满足业务的需求。
  • ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用 ThreadPoolExecutor 可以创建自定义线程池

4. submit() 和 execute() 方法有什么区别?

相同点:都可以开启线程执行线程池中的任务

不同点:

分类 submit execute
接收参数 submit()可以执行 Runnable 和 Callable 类型的任务。 execute()只能执行 Runnable 类型的任务;
返回值 submit()方法可以返回持有计算结果的 Future 对象,同时还可以抛出异常 execute()不可以

即submit()方法用于需要提交返回值的任务,execute()方法用于提交不需要返回值的任务。

5. 常用四种构建线程池的区别及特点?

执行器(Executors)类有许多静态工厂方法,用来构造线程池,并提供了一些静态工厂方法,生成一些常用的线程池。
提供常用四种线程池:

方法 描述
newCachedThreadPool 创建一个可缓存线程池,会立即执行各个任务,如果线程池长度超过需要,可灵活回收空闲线程,若无可回收,则新建线程。空闲线程可保留60秒
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。空闲线程会一直保留
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。用于调度执行的固定线程池
newSingleThreadExecutor 创建一个单线程化的线程池,它只用唯一的工程线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行

【实例】

newCachedThreadPool :

    public static void main(String[] args) {
        // 创建无限大小的线程池,由jvm自动回收
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            executorService.execute(new Runnable() {
                public void run() {
                    try{
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+",i=="+temp);
                }
            });
            
        }
    }
  • 特点:newCachedThreadPool 创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时,它可以灵活的添加新的线程,而不会对池的长度做任何限制
  • 缺点:虽然可以无限制的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为Integer.MAX_VALUE, 一般来说机器没有那么大内存给它不断使用。
  • 总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

newFixedThreadPool

 public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            final int temp = i;
            executorService.execute(new Runnable() {
                public void run() {
                    try{
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+",i=="+temp);
                }
            });

        }
    }
  • 特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
  • 缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)
  • 总结:请求的积压一定要和分派的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。

newScheduledThreadPool

  public static void main(String[] args) {
        ExecutorService executorService = Executors.newScheduledThreadPool(3);

        for (int i = 0; i < 10; i++) {
            final int temp = i;
            executorService.execute(new Runnable() {
                public void run() {
                    try{
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+",i=="+temp);
                }
            } );

        }
    }
  • 特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类)
  • 确定:由于所有任务都是由同一个线程池来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)

newSingleThreadExecutor

 public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            executorService.execute(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+",i=="+temp);
                    try{
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });

        }
    }
  • 特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO,LIFO,优先级执行)
  • 缺点:缺点的花,很明显,它是单线程的,高并发业务下有点困难。
  • 总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程池来替代它

六、ThreadPoolExecutor

java.util.concurrent.ThreadPoolExecutor 类 就是一个线程池。

1.自定义线程池?

【实例】
使用Runnable+ThreadPoolExecutor :自定义线程池

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)

/**
 * @Author: LiangYiFeng
 * @Description 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @Date: Create in 2022/8/15 11:54
 * @Modified By:
 */
public class MyRunnable  implements Runnable{


    private  String command;

    public MyRunnable(String command) {
        this.command = command;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "Start.Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + "End.Time = " + new Date());

    }

    private void processCommand() {
        try{
            Thread.sleep(5000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return "MyRunnable{" +
                "command='" + command + '\'' +
                '}';
    }
}

编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;


    public static void main(String[] args) {
        // 使用阿里巴巴 推荐的创建线程池的方式
        // 通过ThreadPoolExecutor 构造函数自定义参数创建
        ThreadPoolExecutor executor =  new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            Runnable worker = new MyRunnable("" + i);
            executor.execute(worker);

        }

        // 终止线程池
        executor.shutdown();
        while (!executor.isTerminated()){

        }
        System.out.println("Finish all Threads");
    }
    

}

输出结果:

E:\JDK1.8\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=57388:D:\IDEA\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath E:\JDK1.8\jre\lib\charsets.jar;E:\JDK1.8\jre\lib\deploy.jar;E:\JDK1.8\jre\lib\ext\access-bridge-64.jar;E:\JDK1.8\jre\lib\ext\cldrdata.jar;E:\JDK1.8\jre\lib\ext\dnsns.jar;E:\JDK1.8\jre\lib\ext\jaccess.jar;E:\JDK1.8\jre\lib\ext\jfxrt.jar;E:\JDK1.8\jre\lib\ext\localedata.jar;E:\JDK1.8\jre\lib\ext\nashorn.jar;E:\JDK1.8\jre\lib\ext\sunec.jar;E:\JDK1.8\jre\lib\ext\sunjce_provider.jar;E:\JDK1.8\jre\lib\ext\sunmscapi.jar;E:\JDK1.8\jre\lib\ext\sunpkcs11.jar;E:\JDK1.8\jre\lib\ext\zipfs.jar;E:\JDK1.8\jre\lib\javaws.jar;E:\JDK1.8\jre\lib\jce.jar;E:\JDK1.8\jre\lib\jfr.jar;E:\JDK1.8\jre\lib\jfxswt.jar;E:\JDK1.8\jre\lib\jsse.jar;E:\JDK1.8\jre\lib\management-agent.jar;E:\JDK1.8\jre\lib\plugin.jar;E:\JDK1.8\jre\lib\resources.jar;E:\JDK1.8\jre\lib\rt.jar;G:\项目文件\java_se\java_thread\target\classes com.lyf.thread.threadpool.threadpooldemo.ThreadPoolExecutorDemo
pool-1-thread-4Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:35 CST 2022
Finish all Threads

1.ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数:

corePoolSize :用于执行核心线程大小,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize :线程池中允许存在的工作线程的最大数量
workQueue:工作队列的阻塞队列,它相当于生产者-消费者模式中的传输通道。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit :keepAliveTime 参数的时间单位。
keepAliveTime 和 unit 合在一起用于指定线程池中空间线程的最大存活时间。
threadFactory:为线程池提供创建新线程的线程工厂
handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

2.ThreadPoolExecutor饱和策略

ThreadPoolExecutor 自身提供了几个线程的RejectedExecutionException(饱和策略) 接口实现类:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,
ThreadPoolTaskExecutor 定义一些策略:

实现类 所实现的处理策略
ThreadPoolExecutor.AbortPolicy 直接抛出RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.DiscardPolicy 丢弃当前被拒绝任务(而不抛出任何异常)
ThreadPoolExecutor.DiscardOldestPolicy 将工作队列中的最老的任务丢弃,然后重新尝试接纳被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy 在客户端线程执行被拒绝的任务

举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

七、Executors和ThreaPoolExecutor创建线程池的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 各个方法的弊端:

  • newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定

你可能感兴趣的:(java)