欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JUC编程:线程池的深入

程序员文章站 2022-05-02 11:41:31
...

JUC,指的是java util下 concurrent包下面的这些类,通过运用这些类进行并发编程,其中工作中常常了解的就是线程池了

1 什么是线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动

池化技术有很多应用,例如数据库连接池,常量池,redis连接池等等,池化技术带来的好处也是显而易见的。

2 线程池的运用

线程池的创建有常用的三种方式

1) 固定线程池

固定线程池顾名思义就是线程池中线程数据是固定的

、//创建最大线程数目为10的固定线程池
xecutorService executorService = Executors.newFixedThreadPool(10);

2)缓存线程池

这是一个可伸缩的线程池,会根据需要不断创建线程去执行

 ExecutorService executorService = Executors.newCachedThreadPool();

3)单线程

 ExecutorService executorService = Executors.newSingleThreadExecutor();

 其实在工作中,上面这几种创建方式是不允许使用的,具体可看阿里巴巴开发手册,其实看源码就能发现问题

接下来走进源码,会发现这三种方式的创建都是同一个方法

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

3 七大参数

corePoolSize:核心线程数,当线程池初始化的时候这些线程就会被初始化,随时待命。好比我们去银行办理业务,总共有十个窗口,但是有三个一直在办理业务,其他窗口可能会暂时停止服务,这三个就是核心线程数

maximumPoolSize:最大线程数,好比银行里面所有的办理窗口,最大办理业务就这么多了。

keepAliveTime:线程存活时间,当超过这个时间任务调度,线程就会被回收。好比银行窗口超过一个小时没人办理,那么窗口就会挂个牌子告诉你暂停业务办理,去其他窗口办理去。这个时间参数也就是在线程总数超过核心线程数的时候起作用,主要针对于那些非核心线程的。

unit:时间单位,没什么好说的

workQueue:任务队列,线程执行的任务都会被扔到这里。好比你去银行办业务,银行提供的等候区座位,当来一个人办理业务,当窗口有人办理时,你就要去等候区等待,线程也一样,当核心线程被占用时,任务需提交到队列中等待。队列的话后面单独讲,有多重策略实现。

defaultThreadFactory:线程工厂,一般都是用默认生产线程的工厂,具体有什么差别,后续再说

defaultHandler:拒绝策略。有四种,不过也可以自定义实现,默认的是AbortPolicy,如果不清楚这种策略是什么意思,来 看源码

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }

很显然,,直接抛出异常,这个任务执行不了。其他四种看源码一看便知(我觉得看源码学习是最好的学习方式)

怎么理解这个拒绝策略了。继续拿上面的例子说:你去银行办理业务,十个窗口都满了,等候区也满了,那么你再来银行就没地方站了,银行可能保安会对你说:小伙子,走吧,我们这满了。

代码层面理解:当前线程数量大于最大线程数+任务队列数 则拒绝策略生效,可以代码实现来证明这个问题:

import java.util.concurrent.*;

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                       (2,
                       10, 2,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(10),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy());

        try {
            for (int i = 0; i < 25; i++) {
                threadPoolExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("当前线程存活数:" + Thread.activeCount() + ",当前线程名称:" + Thread.currentThread().getName() + "");
                    }
                });
            }
        } finally {
            //关闭
            threadPoolExecutor.shutdown();
        }
    }
}

打印日志:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task [email protected] rejected from [email protected][Running, pool size = 10, active threads = 6, queued tasks = 0, completed tasks = 18]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.proxy.demo1.Demo1.main(Demo1.java:18)

多执行几遍会发现,拒绝策略生效,也就是当任务队列满了,最大线程都在忙着,这时候再来任务,拒绝策略就生效了

4 为什么工作中需要自定义线程池参数

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

这是创建固定线程池的源码,会发现任务队列LinkedBlockingQueue,点进去看他的初始大小,

 /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

 可以添加Integer.MAX_VALUE数量级的任务,难道不会引发OOM吗。

再例如缓存线程池的源码:

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

大家可以看到最大线程数为Integer.MAX_VALUE,这可不得了啊,无限制的膨胀,不受控制 那还得了。

所以实际项目开发中都是自己配置线程池的参数,参数的配置根据项目的需要来进行配置,一般最大线程数这个值不是固定的,不是代码中写死的,可能项目在不同机器上的配置不一样,cpu核心数也不一样,所以这个值最好动态获取

任务呢一般为两种:cpu密集型和IO密集型,cpu密集型的话最大线程数为cpu数量即可,io密集型一般为cpu最大核心数的两倍吧,总之要比cpu核心数目多。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                       (    
                        //核心线程数,这个根据自身情况定
                        4,
                        //最大线程数,一般用cpu数目,这个可以动态获取
                        Runtime.getRuntime().availableProcessors(), 
                        //时间
                        2,
                        //时间单位
                        TimeUnit.MICROSECONDS,
                        //任务队列,记得设置初始大小
                        new LinkedBlockingQueue<>(20),
                        //一般选择默认的就行
                        Executors.defaultThreadFactory(),
                        //拒绝策略,根据自身需要选择,如果四种都不满足,可以自定义实现
                        new ThreadPoolExecutor.AbortPolicy());

总结:线程池的使用最好自定义参数去实现,理解里面七个参数。其他知识后续讲解

相关标签: 后端开发