线程池你真不来了解一下吗?

前言

只有光头才能变强

回顾前面:java

本篇主要是讲解线程池,这是我在多线程的倒数第二篇了,后面还会有一篇死锁。主要将多线程的基础过一遍,之后有机会再继续深刻算法

那么接下来就开始吧,若是文章有错误的地方请你们多多包涵,不吝在评论区指正哦~编程

声明:本文使用JDK1.8

1、线程池简介

线程池能够看作是线程的集合。在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁)。这样就实现了线程的重用c#

咱们来看看若是没有使用线程池的状况是这样的:api

  • 为每一个请求都新开一个线程
public class ThreadPerTaskWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            // 为每一个请求都建立一个新的线程
            final Socket connection = socket.accept();
            Runnable task = () -> handleRequest(connection);
            new Thread(task).start();
        }
    }
    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

为每一个请求都开一个新的线程虽然理论上是能够的,可是会有缺点微信

  • 线程生命周期的开销很是高。每一个线程都有本身的生命周期,建立和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,而且还会有某些空闲线程也会占用资源
  • 程序的稳定性和健壮性会降低,每一个请求开一个线程。若是受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了。

因此说:咱们的线程最好是交由线程池来管理,这样能够减小对线程生命周期的管理,必定程度上提升性能。多线程

2、JDK提供的线程池API

JDK给咱们提供了Excutor框架来使用线程池,它是线程池的基础架构

  • Executor提供了一种将“任务提交”与“任务执行”分离开来的机制(解耦)

下面咱们来看看JDK线程池的整体api架构:并发

接下来咱们把这些API都过一遍看看:框架

Executor接口:

ExcutorService接口:

AbstractExecutorService类:

ScheduledExecutorService接口:

ThreadPoolExecutor类:

ScheduledThreadPoolExecutor类:

2.1ForkJoinPool线程池

除了ScheduledThreadPoolExecutor和ThreadPoolExecutor类线程池之外,还有一个是JDK1.7新增的线程池:ForkJoinPool线程池

因而咱们的类图就能够变得完整一些:

JDK1.7中新增的一个线程池,与ThreadPoolExecutor同样,一样继承了AbstractExecutorService。ForkJoinPool是Fork/Join框架的两大核心类之一。与其它类型的ExecutorService相比, 其主要的不一样在于采用了工做窃取算法(work-stealing):全部池中线程会尝试找到并执行已被提交到池中的或由其余线程建立的任务。这样不多有线程会处于空闲状态,很是高效。这使得可以有效地处理如下情景: 大多数由任务产生大量子任务的状况;从外部客户端大量提交小任务到池中的状况。

来源:

2.2补充:Callable和Future

学到了线程池,咱们能够很容易地发现:不少的API都有Callable和Future这么两个东西。

Future<?> submit(Runnable task)
    <T> Future<T> submit(Callable<T> task)

其实它们也不是什么高深的东西~~~

咱们能够简单认为:Callable就是Runnable的扩展

  • Runnable没有返回值,不能抛出受检查的异常,而Callable能够

也就是说:当咱们的任务须要返回值的时,咱们就可使用Callable!

Future通常咱们认为是Callable的返回值,但他其实表明的是任务的生命周期(固然了,它是能获取获得Callable的返回值的)

简单来看一下他们的用法:

public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 建立线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 能够执行Runnable对象或者Callable对象表明的线程
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        Future<Integer> f2 = pool.submit(new MyCallable(200));

        // V get()
        Integer i1 = f1.get();
        Integer i2 = f2.get();

        System.out.println(i1);
        System.out.println(i2);

        // 结束
        pool.shutdown();
    }
}

Callable任务:

public class MyCallable implements Callable<Integer> {

    private int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }

}

执行完任务以后能够获取获得任务返回的数据

3、ThreadPoolExecutor详解

这是用得最多的线程池,因此本文会重点讲解它。

咱们来看看顶部注释:

3.1内部状态

变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息

线程的状态:

  • RUNNING:线程池可以接受新任务,以及对新添加的任务进行处理。
  • SHUTDOWN:线程池不能够接受新任务,可是能够对已添加的任务进行处理。
  • STOP:线程池不接收新任务,不处理已添加的任务,而且会中断正在处理的任务
  • TIDYING:当全部的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;能够经过重载terminated()函数来实现。
  • TERMINATED:线程池完全终止的状态

各个状态之间转换:

3.2已默认实现的池

下面我就列举三个比较常见的实现池:

  • newFixedThreadPool
  • newCachedThreadPool
  • SingleThreadExecutor

若是读懂了上面对应的策略呀,线程数量这些,应该就不会太难看懂了。

3.2.1newFixedThreadPool

一个固定线程数的线程池,它将返回一个corePoolSize和maximumPoolSize相等的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

3.2.2newCachedThreadPool

很是有弹性的线程池,对于新的任务,若是此时线程池里没有空闲线程,线程池会坚决果断的建立一条新的线程去处理这个任务

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

3.2.3SingleThreadExecutor

使用单个worker线程的Executor

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

3.3构造方法

咱们读完上面的默认实现池还有对应的属性,再回到构造方法看看

  • 构造方法可让咱们自定义(扩展)线程池
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;
    }
  1. 指定核心线程数量
  2. 指定最大线程数量
  3. 容许线程空闲时间
  4. 时间对象
  5. 阻塞队列
  6. 线程工厂
  7. 任务拒绝策略

再总结一遍这些参数的要点:

线程数量要点

  • 若是运行线程的数量少于核心线程数量,则建立新的线程处理请求
  • 若是运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才建立新的线程
  • 若是核心线程数量等于最大线程数量,那么将建立固定大小的链接池
  • 若是设置了最大线程数量为无穷,那么容许线程池适合任意的并发数量

线程空闲时间要点:

  • 当前线程数大于核心线程数,若是空闲时间已经超过了,那该线程会销毁

排队策略要点

  • 同步移交:不会放到队列中,而是等待线程执行它。若是当前线程没有执行,极可能会新开一个线程执行。
  • 无界限策略:若是核心线程都在工做,该线程会放到队列中。因此线程数不会超过核心线程数
  • 有界限策略:能够避免资源耗尽,可是必定程度上减低了吞吐量

当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的状况了:

拒绝任务策略:

  • 直接抛出异常
  • 使用调用者的线程来处理
  • 直接丢掉这个任务
  • 丢掉最老的任务

4、execute执行方法

execute执行方法分了三步,以注释的方式写在代码上了~

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //若是线程池中运行的线程数量<corePoolSize,则建立新线程来处理请求,即便其余辅助线程是空闲的。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        //若是线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,
            // 1.若是线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。
            // 2.不然若是线程池中运行的线程数量为0,则经过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 若是以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。
        else if (!addWorker(command, false))
            reject(command);
    }

5、线程池关闭

ThreadPoolExecutor提供了shutdown()shutdownNow()两个方法来关闭线程池

shutdown() :

shutdownNow():

区别:

  • 调用shutdown()后,线程池状态马上变为SHUTDOWN,而调用shutdownNow(),线程池状态马上变为STOP
  • shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。

6、总结

本篇博文主要简单地将多线程的结构体系过了一篇,讲了最经常使用的ThreadPoolExecutor线程池是怎么使用的~~~

明天但愿能够把死锁写出来,敬请期待~~~

还有剩下的几个线程池(给出了参考资料):

参考资料:

若是文章有错的地方欢迎指正,你们互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同窗,能够 关注微信公众号:Java3y。为了你们方便,刚新建了一下 qq群:742919422,你们也能够去交流交流。谢谢支持了!但愿能多介绍给其余有须要的朋友

文章的目录导航

相关文章
相关标签/搜索