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)]

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

粗略源码分析:

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

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

主要的方法逻辑是在runworker中。
在这里插入图片描述

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

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

七、线程池的状态

在这里插入图片描述

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

参考

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/java%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md#3-%E5%BC%82%E6%AD%A5%E8%AE%A1%E7%AE%97%E7%9A%84%E7%BB%93%E6%9E%9Cfuture

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值