4

1.线程池简介

2.线程池使用

3.线程池参数详解

4.如何合理设置线程池参数

1.线程池简介
我们先来介绍一下,什么是线程池?
在以往的博客中,我们介绍线程的时候,无论是Runnable,还是Calllable这些,最终都是使用new Thread().start(); 的方式进行创建,其实在真实的生产环境中,我们是禁止使用这种写法的,因为这样有几个严重的弊端:
a.每次new Thread() 新建对象,会造成开销,导致服务器运行性能变差。
b.在无限制new Thread()的情况下,我们一不小心就会创建过多的线程,相互之间竞争资源,可能导致上下文频繁切换,占用资源过多,死锁或者oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。

于是,JAVA线程池就产生了!

JAVA线程池做的主要工作是控制运行的线程的数量,处理过程中将任务放入队列然后在线程创建后启动这些任务,如果线程数量超过了最大执行数量,超出数量的线程将排队等候,等待其它线程执行完毕,再从队列中取出任务来执行

它的主要特点:线程服用,控制最大并发数,管理线程。

第一:降低资源消耗,通过重复利用自己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一地进行分配,调优和监控。

多说无用,我们来看看如何使用JAVA线程池吧!

2.线程池使用
我们先介绍一下JAVA自带的创建线程池的工具类!

Executors

它提供了三个方法来创建线程池:

        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池固定数线程
        ExecutorService threadPoo2 = Executors.newCachedThreadPool();//一池多线程
        ExecutorService threadPoo3 = Executors.newSingleThreadExecutor();//一池一线程

顾名思义:
newFixedThreadPool()方法就是创建一个固定线程数的线程池,
newCachedThreadPool()方法会根据你计算机和任务的情况,进行自动增减线程
newSingleThreadExecutor()创建一个单线程的线程池,每次只能执行一个任务

同时创建完线程池之后,使用线程池自带的submit或者excute方法,然后用lambda表达式传入函数体后,就可以执行任务了!

        threadPool.submit();
        threadPool.execute();

这时,我们可以使用一个线程池尝试着使用一下

        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池固定数线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

我们创建了一个线程池,固定数量为5,然后启动10000个线程,看看运行情况。
image.png

可以看出,线程编号始终没有超过我们设置的初始值5。

这时,我们可以换一个线程池使用一下,我们使用cache线程池:

 
        ExecutorService threadPoo1 = Executors.newCachedThreadPool();//一池多线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoo1.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoo1.shutdown();
        }

image.png
可以看出,线程启动地越多,该线程池就会启动越多的线程来适应需求。

让我们来看一下最后一个线程池:

        ExecutorService threadPoo1 = Executors.newSingleThreadExecutor();//一池一线程
        //模拟10000个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoo1.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoo1.shutdown();
        }

查看结果:
image.png
可以看出,永远只有一个线程在运行。

是不是非常神奇?!我们可以看一下创建线程池的方法到底怎么执行地,居然有这种效果!

首先看一下固定数线程的方法:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, //常驻核心线程数
                                      nThreads,//最大线程数
                                      0L,//多余的空闲线程的存活时间 
                                      TimeUnit.MILLISECONDS,//多余线程存活单位
                                      new LinkedBlockingQueue<Runnable>()); //使用的阻塞队列
    }

cache线程池的方法:

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

单线程池的方法:

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

从我给的源码注释中可以看出,
固定数线程池最大和常驻线程数都是传入的参数
而cache线程池的最大线程数是Integer.MAX_VALUE
单线程池底层使用了SynchronousQueue : 不存储元素的阻塞队列,即单个元素的队列。
多线程池的底层使用了LinkedBlockingQueue : 由链表组成的有界(但是默认值为Integer.MAX_VALUE阻塞队列
阻塞队列可以看我之前写的这篇文章:JAVA并发编程——阻塞队列理论及其API介绍
所以就导致了为什么会有以上的运行结果!

所以在生产中,我们究竟如何使用呢??

答案是————一个都不使用,因为LinkedBlockingQueue它的最大长度是Integer.MAX_VALUE,那就相当于无界可能会让服务器内存爆仓产生OOM,所以接下来,我们学习一下自定义创建线程池的参数!

3.线程池参数详解
我们先点到ThreadPoolExecutor这个类里看一下,它究竟有几个参数:

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

我们可以看到,有七个参数
corePoolSize 线程池中常驻核心线程数
maximumPoolSize 线程池能够容纳的同时执行的最大线程数,此值必须大于等于1
keppAliveTime 多余的空闲线程的存活时间
unit 存活时间单位
workQueue 任务队列,被提交但是尚未执行的任务
threadFactory 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可
handler 拒绝策略,表示队列满了并且工作线程大于等于线程池的最大线程数,如何处理。

前面几个参数都好理解,重要的是这个方法可以自定义阻塞队列的类型,我们就可以避免使用LinkedBlockingQueue了

然后,常驻核心线程数是什么时候才会增加为线程池能够容纳的同时执行的最大线程数呢?这个和我们使用的阻塞队列有关系:
1)synchronousQueue:无缓冲等待队列,是一个不存储的元素阻塞队列,当有任务且最大线程数未满的时候,直接创建线程执行。

2)ArrayBlockingQueue:有界缓存队列,可以指定缓存数量,当超出缓存数量且最大线程数未满的时候,会创建线程执行。

3)LinkedBlockingQueue:无界缓冲队列,剩余的元素会在阻塞队列中等待,只会创建corePoolSize个线程。

接下来,我们看一下handler拒绝策略:
当核心线程数满了,阻塞队列也满了的时候,再进来的线程,就会启动拒绝策略,一般常见的拒绝策略有这几种:

AbortPolicy(默认):直接抛出RejectExcutionException异常阻止线程运行。
CallerRunsPolicy:将多出来的任务退还给调用者,从而降低流量。

DiscardOldestPolicy:抛弃等待队列中等待最久的任务,然后把当前任务加入等待队列。

DiscardPolicy:直接丢弃任务

接下来,我们尝试一下自己新建线程池,并尝试着使用该线程池执行一下任务。

     ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,//常驻核心线程数
                5,//最大核心线程数
                1L,//多余的空闲线程的存活时间
                TimeUnit.SECONDS,//多余空闲线程存活时间单位
                new ArrayBlockingQueue<Runnable>(10),//阻塞队列
                Executors.defaultThreadFactory(),//生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.AbortPolicy());//拒绝策略

        //模拟多个用户来办理业务,每一个用户就是来自外部的请求线程
        try {
            for (int i = 1; i < 10000; i++) {
                threadPoolExecutor.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }

在使用当前拒绝策略下,很明显可以看出,执行线程超出线程池的数量了,会抛异常,我们来看一下运行结果:
image.png

我们可以换一个拒绝策略试一下:比如说DiscardPolicy(抛弃任务)

        ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,//常驻核心线程数
                5,//最大核心线程数
                1L,//多余的空闲线程的存活时间
                TimeUnit.SECONDS,//多余空闲线程存活时间单位
                new ArrayBlockingQueue<Runnable>(10),//阻塞队列
                Executors.defaultThreadFactory(),//生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.DiscardPolicy());//拒绝策略

image.png
它就运行完成了,且没有抛出任何异常。

4.如何合理设置线程池参数
如何配置线程池的参数,其实我们最需要配置的参数只有一个:
corePoolSize 线程池中常驻核心线程数

当我们的线程池使用场景是在CPU密集型(进程绝大部份任务依靠cpu的计算能力完成),那么为了防止上下文的切换造成的开销,我们一般设置最大线程数等于CPU核数(核数获取方法:Runtime.getRuntime().availableProcessors();)

当我们的线程池使用场景是在IO密集型(绝大部分任务就是在读入,输出数据),那么cpu不是非常非常繁忙,大部分在等待的情况下,最大线程可以是CPU核数的两倍。

5.总结
这次我们介绍了线程池的原理以及参数详解,最重要的是,创建线程池必须要手动使用构造方法创建,不能使用默认的方法类进行创建!

最后,我们用两张图来概括一下线程池的执行流程:

image.png

image.png

好了,今天的分享就到这里!


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。