线程池

手动创建线程的缺点

线程需要消耗大量资源;线程的数量也不是越多越好

  1. 系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建标准不一样(比如线程没有名字)。当系统运行起来,所有线程都在疯狂抢占资源,会引发混乱——不受控风险
  2. 过多的线程也会引起上下文切换的开销
  3. Thread是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源

创建线程过程

Java创建线程语法new Thread()在操作系统层面并没有创建新的线程,这是编程语言特有的。真正转换为操作系统层面创建一个线程,还要调用操作系统内核的APIstart()方法),然后操作系统要为该线程分配一系列的资源!

Object obj = new Object()过程:

  1. 分配一块内存M;
  2. 在内存M上初始化该对象;
  3. 将内存M的地址赋值给引用变量obj。

创建一个线程的过程:

  1. JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧。每一栈帧由一个局部变量数组、返回值、操作数堆栈组成;
  2. 一些支持本机方法的JVM也会分配一个本机堆栈
  3. 每个线程获得一个程序计数器,告诉它当前处理器执行的指令是什么;
  4. 系统创建一个与Java线程对应的本机线程
  5. 将与线程相关的描述符添加到JVM内部数据结构中;
  6. 线程共享方法区

创建一个线程(即便不干什么)需要大约1M左右!因此频繁手动创建、销毁线程的代价是非常巨大的!

线程池作用

Java线程池就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理。

  1. 利用线程池管理并复用线程控制最大并发数(手动创建线程很难得到保证);
  2. 实现任务线程队列缓存策略拒绝机制
  3. 实现某些与时间相关的功能,如定时执行周期执行等(比如列车指定时间运行);
  4. 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大。因此,通过配置独立的线程池,将较慢的交易服务与搜索服务个离开,避免个服务线程互相影响

ThreadPoolExecutor

三大方法

三大方法本质都是调用ThreadPoolExecutor

// 1.单个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor(); 
// 源码Executors#newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

// 2.创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newFixedThreadPool(5);     
// 源码Executors#newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// 3.可伸缩的,遇强则强,遇弱则弱
ExecutorService threadPool = Executors.newCachedThreadPool(); 
// 源码Executors#newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

实例:

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

public class Pool1
{
    public static void main(String[] args) {
        // ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单个线程
        // ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
        try {
            for (int i = 0; i < 100; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}
/*
ExecutorService threadPool = Executors.newSingleThreadExecutor()
...
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
...

ExecutorService threadPool = Executors.newFixedThreadPool(5);
...
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-5 ok
pool-1-thread-5 ok
pool-1-thread-4 ok
...

ExecutorService threadPool = Executors.newCachedThreadPool();
...
pool-1-thread-6 ok
pool-1-thread-23 ok
pool-1-thread-8 ok
pool-1-thread-9 ok
pool-1-thread-37 ok
pool-1-thread-36 ok
...
 */

七大参数

Java线程池就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险将多个线程统一在一起管理

了解了线程池的几个核心参数概念后,需要经过调优的过程来设置最佳线程参数值

public class ThreadPoolExecutor extends AbstractExecutorService {
    // 七大参数
    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;
    }
}

为了形象的理解ThreadPoolExecutor七大参数

序号 参数名称 参数解释 春运形象说明
1 corePoolSize 表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁 日常固定的列车数辆(不管是不是春运,都要有固定车次运行)
2 maximumPoolSize 表示线程池能够容纳可同时执行的最大线程数(结合workQueue 春运客流量大,临时加车,加车后,总列车次数不能超过这个最大值,否则就会出现调度不开等问题 (结合workQueue
3 keepAliveTime 表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下corePoolSize个线程位置 春运压力过后,临时的加车(如果空闲时间超过keepAliveTime)就会被撤掉,只保留日常固定的列车车次数量用于日常运营
4 unit keepAliveTime的时间单位,最终都会转换成纳秒,因为CPU的执行速度杠杠滴 keepAliveTime的单位,春运以天为计算单位
5 workQueue 当请求的线程数大于corePoolSize时,线程进入该阻塞队列 春运压力异常大,达到corePoolSize也不能满足要求,所有乘坐请求都会进入该阻塞队列中排队,队列满,还有额外请求,就需要加车了
6 threadFactory 顾名思义,线程工厂,用来生产一组相同任务的线程,同时也可以通过它增加前缀名,虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的 比如(北京——上海)就属于该段列车所有前缀,表明列车运输职责
7 handler 执行拒绝策略,当workQueue达到上限,同时也达到maximumPoolSize就要通过这个来处理,比如拒绝、丢弃等,这是一种限流的保护措施 workQueue排队也达到队列最大上线,maximumPoolSize就要提示无票等拒绝策略了,因为我们不能加车了,当前所有车次已经满负载

ThreadPoolExecutor工作情景

《阿里巴巴Java手册》建议:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,可以更明确线程池的运行规则,规避资源耗尽的风险。

  • 情形一:当正在运行的线程数(包括空闲线程数)\(<\)corePoolSize时,新建线程执行任务
  • 情形二:当正在运行的线程数\(\geq\)corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行
  • 情形三:当队列里的任务达到上限,并且正在进行的线程\(<\)maxinumPoolSize,对于新加入的任务,新建线程
  • 情形四:当队列里的任务达到上限,并且正在运行的线程\(=\)maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的策略是抛异常)。

情形一

当池中正在运行的线程数(包括空闲线程数)小于corePoolSize时,新建线程执行任务
If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.

package pool;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, 3, 60L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
        );

        // 任务1
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务1!"));

        // 任务2
        threadPool.execute(
                () -> {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println(Thread.currentThread().getName() + " 执行任务2!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );

        threadPool.shutdown();
    }
}

输出结果:

pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!

当执行任务1的线程(thread-1)执行完成之后,任务2并没有去复用thread-1而是新建线程(thread-2)去执行任务!

情形二

当池中正在运行的线程数大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许),等待空闲线程来执行
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.

package pool;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, 3, 60L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
        );

        // 任务1
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务1!"));

        // 任务2
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务2!"));

        // 任务3
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));

        threadPool.shutdown();
    }
}

输出结果:

pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-1 执行任务3!

线程池不会为任务3新建线程,因为有一个空的队列,先将其放入队列中,等thread-1执行完任务1,再去执行任务3。此时,maximumPoolSize=3这个参数不起作用。

情形三

当队列里的任务达到上限,并且正在进行的线程小于maxinumPoolSize,对于新加入的任务,新建线程

package pool;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, 3, 60L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
        );

        // 任务1
        threadPool.execute(
                () -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 执行任务1!");
                        TimeUnit.SECONDS.sleep(1); // 注意这里需要加延时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );

        // 任务2
        threadPool.execute(
                () -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 执行任务2!");
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );

        // 任务3
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));

        // 任务4
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务4!"));

        threadPool.shutdown();
    }
}

输出结果:

pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-3 执行任务4!
pool-1-thread-3 执行任务3!

任务1、2启动后尚未结束,此时任务3在队列,队列里的任务达到上限,由于正在进行的线程数是2<maximumPoolSize,只能新建一个线程了。然后任务4就进了新线程thread-3,任务4结束,队列里的任务3在线程thread-3进行处理。

情形四

队列里的任务达到上限,并且池中正在运行的线程等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的策略是抛异常)

package pool;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, 3, 60L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)
        );

        // 任务1
        threadPool.execute(
                () -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 执行任务1!");
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );

        // 任务2
        threadPool.execute(
                () -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 执行任务2!");
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );

        // 任务3
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3!"));

        // 任务4
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务4!"));

        // 任务5
        threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 执行任务5!"));

        threadPool.shutdown();
    }
}

输出结果:

pool-1-thread-1 执行任务1!
pool-1-thread-2 执行任务2!
pool-1-thread-3 执行任务4!
pool-1-thread-3 执行任务3!
Exception in thread "main" java.util.concurrent.RejectedExecutionException

队列达到上限,线程池达到最大值,故执行拒绝策略,抛出异常。

四大拒绝策略

workQueue达到上限,同时也达到maximumPoolSize就要通过这个来处理:

  • new ThreadPoolExecutor.AbortPolicy()

    • 默认拒绝策略
    • 丢弃任务并抛出RejectedExecutionException异常
    • 如果是比较关键的业务,推荐使用此拒绝策略,这样在系统不能承载更大的并发量的时候,能够及时的通过异常发现
  • new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!

    • 由调用线程(提交任务的线程)处理该任务
  • new ThreadPoolExecutor.DiscardPolicy()

    • 直接丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
    • 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,博客网站统计阅读量就是采用的这种拒绝策略。
  • new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的竞争,也不会抛出异常!

    • 把最早进入工作队列的任务(等待最久)丢弃,然后把新任务加入到工作队列

为什么不推荐使用Executors

// 1.单个线程
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

// 2.创建一个固定的线程池的大小
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// 3.可伸缩的,遇强则强,遇弱则弱
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

线程池静态工厂Executors的三大方法本质都是调用ThreadPoolExecutor,并且传入的workQueue是一个边界为Integer.MAX_VALUE的无界队列,因为边界太大了,这么大的等待队列非常消耗内存,存在资源耗尽的风险

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

另外调用的ThreadPoolExecutor方法使用的是默认拒绝策略(直接拒绝),但并不是所有业务场景都适合使用这个策略,当很重要的请求过来,直接选择拒绝,显然是不合适的

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

总之,使用Executors创建的线程池太过于理想化,并不能满足很多现实中的业务场景:Executors中默认的线程工厂拒绝策路过于简单,通常对用户不够友好。

  • 线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号。
  • 拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转

所以要求我们通过ThreadPoolExecutor来创建,并传入合适的参数

posted @ 2021-06-27 11:02  chenzufeng  阅读(98)  评论(1)    收藏  举报