资讯专栏INFORMATION COLUMN

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

stdying / 1407人阅读

摘要:所以说我们的线程最好是交由线程池来管理,这样可以减少对线程生命周期的管理,一定程度上提高性能。线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。当所有的任务已终止,记录的任务数量为,线程池会变为状态。线程池彻底终止的状态。

前言
只有光头才能变强

回顾前面:

ThreadLocal就是这么简单

多线程三分钟就可以入个门了!

多线程基础必要知识点!看了学习多线程事半功倍

Java锁机制了解一下

AQS简简单单过一遍

Lock锁子类了解一下

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

那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~

声明:本文使用JDK1.8
一、线程池简介

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

我们来看看如果没有使用线程池的情况是这样的:

为每个请求都新开一个线程

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
    }
}

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

线程生命周期的开销非常高。每个线程都有自己的生命周期,创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还会有某些空闲线程也会占用资源

程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了。

所以说:我们的线程最好是交由线程池来管理,这样可以减少对线程生命周期的管理,一定程度上提高性能。

二、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):所有池中线程会尝试找到并执行已被提交到池中的或由其他线程创建的任务。这样很少有线程会处于空闲状态,非常高效。这使得能够有效地处理以下情景:大多数由任务产生大量子任务的情况;从外部客户端大量提交小任务到池中的情况。

来源:

https://blog.csdn.net/panweiwei1994/article/details/78969238

2.2补充:Callable和Future

学到了线程池,我们可以很容易地发现:很多的API都有Callable和Future这么两个东西。

    Future submit(Runnable task)
     Future submit(Callable 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 f1 = pool.submit(new MyCallable(100));
        Future 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 {

    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;
    }

}

执行完任务之后可以获取得到任务返回的数据

三、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());
    }
3.2.2newCachedThreadPool

非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务

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

使用单个worker线程的Executor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }
3.3构造方法

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

构造方法可以让我们自定义(扩展)线程池

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue 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;
    }

指定核心线程数量

指定最大线程数量

允许线程空闲时间

时间对象

阻塞队列

线程工厂

任务拒绝策略

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

线程数量要点

如果运行线程的数量少于核心线程数量,则创建新的线程处理请求

如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程

如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池

如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量

线程空闲时间要点:

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

排队策略要点

同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。

无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数

有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量

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

拒绝任务策略:

直接抛出异常

使用调用者的线程来处理

直接丢掉这个任务

丢掉最老的任务

四、execute执行方法

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

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int 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);
    }
五、线程池关闭

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

shutdown() :

shutdownNow():

区别:

调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP

shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。

六、总结

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

明天希望可以把死锁写出来,敬请期待~~~

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

ScheduledThreadPoolExecutor

https://blog.csdn.net/panweiwei1994/article/details/78997029

http://cmsblogs.com/?p=2451

ForkJoinPool

https://blog.csdn.net/panweiwei1994/article/details/78992098

参考资料:

《Java核心技术卷一》

《Java并发编程实战》

http://cmsblogs.com/?page_id=111

https://blog.csdn.net/panweiwei1994/article/details/78483167

https://zhuanlan.zhihu.com/p/35382932

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y。为了大家方便,刚新建了一下qq群:742919422,大家也可以去交流交流。谢谢支持了!希望能多介绍给其他有需要的朋友

文章的目录导航

https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69288.html

相关文章

  • 线程之死锁就是这么简单

    摘要:此时线程需要锁才能继续往下执行。但是线程的锁并没有释放,线程的锁也没有释放。 前言 只有光头才能变强 回顾前面: ThreadLocal就是这么简单 多线程三分钟就可以入个门了! 多线程基础必要知识点!看了学习多线程事半功倍 Java锁机制了解一下 AQS简简单单过一遍 Lock锁子类了解一下 线程池你真不来了解一下吗? 本篇主要是讲解死锁,这是我在多线程的最后一篇了。主要将多线程...

    winterdawn 评论0 收藏0
  • 给女朋友讲解什么是代理模式

    摘要:受知乎文章和设计模式之禅的启发,我也来搞一篇脑洞小开的文章由标题可知,这篇文章是写给我女朋友看的。于是这就让经纪人对粉丝说只有万,我才会写代码。 前言 只有光头才能变强 回顾前面: ThreadLocal就是这么简单 多线程三分钟就可以入个门了! 多线程基础必要知识点!看了学习多线程事半功倍 Java锁机制了解一下 AQS简简单单过一遍 Lock锁子类了解一下 线程池你真不来了解一下...

    stormgens 评论0 收藏0
  • 【Java】几道让你拿offer的面试题

    摘要:的方法,的默认实现会判断是否是类型注意自动拆箱,自动装箱问题。适应自旋锁锁竞争是下的,会经过用户态到内核态的切换,是比较花时间的。在中引入了自适应的自旋锁,说明自旋的时间不固定,要不要自旋变得越来越聪明。 前言 只有光头才能变强 之前在刷博客的时候,发现一些写得比较好的博客都会默默收藏起来。最近在查阅补漏,有的知识点比较重要的,但是在之前的博客中还没有写到,于是趁着闲整理一下。 文本的...

    张春雷 评论0 收藏0
  • Java开发 大厂面试整理

    摘要:用户态不能干扰内核态所以指令就有两种特权指令和非特权指令不同的状态对应不同的指令。非特权指令所有程序均可直接使用。用户态常态目态执行非特权指令。 这是我今年从三月份开始,主要的大厂面试经过,有些企业面试的还没来得及整理,可能有些没有带答案就发出来了,还请各位先思考如果是你怎么回答面试官?这篇文章会持续更新,请各位持续关注,希望对你有所帮助! 面试清单 平安产险 飞猪 上汽大通 浩鲸科...

    Scorpion 评论0 收藏0
  • 关于线程池你不得不知道的一些设置

    摘要:有细心的网友早就想到了这个问题在线程池中,还有一些不常用的设置。所以该方法会在线程池总预先创建没有任务执行的线程,数量为。下面我们测试一下从测试结果来看,线程池中已经预先创建了数量的空闲线程。 微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。 老司机倾囊相授,带你一路进阶,来不及解释了快上车! 看完我上一篇文章「你都理解创建线...

    余学文 评论0 收藏0

发表评论

0条评论

stdying

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<