文章目录

  • 一、概述以及大体框架
  • 二、ThreadPoolExecutor
  • 参数分析
  • 阿里推荐使用ThreadPoolExecutor
  • 三、线程池运行流程
  • 四、线程池的一些常用方法
  • Runnable 与 Callable
  • 回顾下线程创建的几种方法
  • execute() 与 submit()
  • 停止线程的几种方法
  • 五、如何设置线程数量
  • CPU密集型(N+1)
  • I/O密集型(2N)
  • 六、为什么线程池里面的线程能够进行复用
  • 七、线程池的状态
  • 参考


一、概述以及大体框架

为什么需要线程池?池化技术?

因为如果每来一个任务,都创建的线程、执行任务、线程,这样重复的操作都带来不少性能开销,因此使用池化技术,在开始执行期间事先准备好若干线程,如果任务来了就从池子里获取线程来执行。

大体框架:

java 多线程 可见性 java多线程 线程池_线程池

Executor只有定义了一个execute方法。

ExecutorService定义了一系列方法,比如shutdown,isshutdown等等方法。

AbstractExecutorService是个抽象类,定义了一系列通用方法。

而ThreadPoolExecutor和ScheduledThreadPoolExecutor为具体线程池的实现类。

二、ThreadPoolExecutor

线程池实现类ThreadPoolExecutor 是 Executor最核心的类。

参数分析

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;
}
  • corePoolSize:线程池核心线程数量
  • maxinumPoolSize:线程池最大线程数量
  • keepLiveTime:非核心线程的存活时间
  • timeunit:存活时间的单位
  • workQueue:存放任务的阻塞队列
  • threadFactory:线程工厂
  • rejectHandler:拒绝策略

其中workQueue阻塞队列ArrayBlockingQueue、LinkBlockingQueue。

threadFactory可以用默认的threadFactory。或者可以用自己创建的线程工厂,线程的名字由自己定义,这样可以跟踪问题。

拒绝策略:当线程池的线程数量等于maxNumPoolSize最大线程数时,并且阻塞队列已满时,再添加任务时会执行拒绝策略:

  • AbortPolicy:拒绝任务,抛出RejectExecutionException异常。
  • DiscardPolicy:直接抛弃,不抛出异常。
  • DiscardOldestPolicy:抛弃等待最久的任务,并把任务添加到任务队列里。
  • CallerRunPolicy:让调用者,也就是主线程去执行这个任务,如果选这个拒绝策略的话,会较低任务提交的速度,会影响程序的性能,但如果程序接受延迟执行任务的话选这个也可以。

阿里推荐使用ThreadPoolExecutor

因为使用Executors也可以创建线程,但是不推荐这样做,在阿里开发者手册有提到:

第一类:

  • newFixedThreadPoolExecutor
  • newSingleThreadPoolExecutor

这两个方法创建的线程池的任务队列都是使用LinkBlockingQueue,LinkBlockingQueue没有设置容量,也就是说我可以向程序不断提交任务,导致任务挤压,最终导致OOM。

第二类:

  • newCachedThreadPoolExecutor
  • newScheduledThreadPool

这两个方法创建的线程池的参数中,他们的最大线程池数量为Interger.max_value,如果不断提交任务,可能会不断创建大量线程,因此也会导致OOM。

三、线程池运行流程

线程池分为核心线程跟非核心线程

一开始提交任务的时候,由于当前线程池的线程数量小于corePoolSize,所以每来一个任务都会创建线程来执行任务,直到线程池的线程数量等于corePoolSize。

如果这个时候再来任务,此时线程数量等于corePoolSize,因此就会加入workQueue阻塞队列,直到workQueue满。

如果此时再来任务,workQueue已经满了,会去判断当前的线程数量是不是小于maxnumPoolSize,如果小于的话,会创建非核心线程处理任务。

如果任务队列满了,并且当前线程数量也等于maxNumPoolSize了,就会执行拒绝策略拒绝任务。

常见的拒绝策略有四种之多,默认是使用AbortPolicy,拒绝执行任务并抛出RejectExecutionException异常。

四、线程池的一些常用方法

Runnable 与 Callable

最重要的特征就是一个有返回值,另外一个没有返回值。

补一个FutureTask的简单实用

回顾下线程创建的几种方法

1.继承Thread,并重写run方法

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread来实现创建线程");
    }
}

2.传入Runnable接口实现类,并用Thread.start来创建线程

Thread myThread = new Thread(() -> System.out.println("hello"));

3.使用FutureTask和Callable接口,并用Thread.start(futureTask)来创建线程

FutureTask futureTask = new FutureTask(new Callable() {
    @Override
    public Object call() throws Exception {
        return null;
    }
});
Thread thread = new Thread(futureTask);
thread.start();

//通过futureTask来获取Callable的返回值
futureTask.get();

4.使用线程池submit Callable或者execute Runnable

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit();  //通过future来获取返回值
executorService.execute();

execute() 与 submit()

execute用于线程池提交任务,但是没有返回值

如果使用submit的话可以使用Future获取返回值

List<Future<String>> futureList = new ArrayList<>();
Callable<String> callable = new MyCallable();
for (int i = 0; i < 10; i++) {
    //提交任务到线程池
    Future<String> future = executor.submit(callable);
    //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
    futureList.add(future);
}
for (Future<String> fut : futureList) {
    try {
        System.out.println(new Date() + "::" + fut.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

停止线程的几种方法

  • shutdown:进入shutdown状态,此时线程池还在运行,会把队列面的任务执行完,但是此时不能添加任务了,任务来了会抛出RejectExecutionException异常。
  • isShutdown:判断是否在shutdown状态。
  • shutdownNow:暴力立马关闭线程池。
  • isTerminated:检查线程池是否已经停止了。

五、如何设置线程数量

如果线程数量少,那么会导致任务执行速度慢,导致任务挤压,可能会因为任务队列出现OOM的情况。

如果线程数量太多的话,线程的竞争大,会导致大量上下文切换,因为CPU是分配给时间片给线程来处理任务的,时间片一到,那么线程就得保存当前任务,等待下一次CPU分配时间片,那么下一次线程获取到CPU时间片之后,会重新加载任务来处理,保存到加载的过程就称之为上问下切换。

CPU密集型(N+1)

如果程序的计算量特别高,那么就属于CPU密集型,线程数量可以设置为N(CPU的核心数)+1。

I/O密集型(2N)

如果程序的是网络传输或者I/O操作比较多,那么线程池应该设置为2N。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKyDHGf6-1600421470606)(22CD8B13C8FF401D8A396024D59B7920)]

六、为什么线程池里面的线程能够进行复用

粗略源码分析:

java 多线程 可见性 java多线程 线程池_线程池_02

当前线程数少于核心线程数时会添加worker,worker实际上就是线程池里面对线程的封装。

java 多线程 可见性 java多线程 线程池_任务队列_03

可以看到worker是ThreadPoolExecutor的一个内部类,里面包含了线程变量,以及一个firstTask,firstTask就是一个runnable的对象了。

主要的方法逻辑是在runworker中。

java 多线程 可见性 java多线程 线程池_任务队列_04

可以看到runWorker会获取当前线程,work里面的firstTask任务,能够轮转的原因就是因为这个while循环了,while循环会去getTask,从任务队列里面获取Task,如果Task不为空,则会执行run方法。

注意这里是run方法,因为start方法是从底层开启一个线程来执行run方法,但是这里面是用的是worker里面的thread来执行,因此没有用到start方法。

七、线程池的状态

java 多线程 可见性 java多线程 线程池_java 多线程 可见性_05

  • Running:代表线程池正在运行。
  • Shutdown:使用shutdown()方法,代表线程池会进入shutdown状态,会继续处理队列中的任务,但是不会接受新任务了。
  • Stop:使用shutdownNow会进入这个状态,停止目前线程池里面的所有任务,并且会返回一个List 表示在队列里面还没有执行的任务。会随后进入到teminated状态。
  • Tidying:整洁,代表此时所有线程都空闲了,并且也没有任务了。
  • Teminated:线程池停止了。