java多线程(6)线程池

池的概念在java中也是常见,还有连接池、常量池等,池的作用也是类似的,对于对象、资源的重复利用,减小系统开销,提升运行效率。

线程池的主要功能:
1.减少创建和销毁线程的次数,提升运行性能,尤其是在大量异步任务时
2.可以更合理地管理线程,如:线程的运行数量,防止同一时间大量任务运行,导致系统崩溃

demo

先举个demo,看看使用线程池的区别,线程池:

AtomicLong al = new AtomicLong(0l);
            Long s1 = System.currentTimeMillis();
            ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 100000, 100, TimeUnit.SECONDS, new LinkedBlockingDeque(20000));
            for(int i=0;i<20000;i++){
                pool.execute(new Runnable() {
                    
                    @Override
                    public void run() {
                        try {
                            al.incrementAndGet();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            pool.shutdown();
            pool.awaitTermination(1, TimeUnit.HOURS);
            System.out.println("耗时:"+(System.currentTimeMillis()-s1));
            System.out.println("AtomicLong="+al.get());

结果:

耗时:224
AtomicLong=20000

非线程池:

AtomicLong al = new AtomicLong(0l);            
            Long s1 = System.currentTimeMillis();
            for(int i=0;i<20000;i++){
                Thread t = new Thread(new Runnable() {
                    
                    @Override
                    public void run() {
                        try {
                            al.incrementAndGet();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                t.start();
            }
            while(true){
                if(al.get()==20000){
                    System.out.println("耗时:"+(System.currentTimeMillis()-s1));
                    System.out.println("AtomicLong="+al.get());
                    break;
                }
                Thread.sleep(1);
            }

结果:

耗时:2972
AtomicLong=20000

从耗时看2者相差了13倍,差距还是挺大的,可见有大量异步任务时使用线程池能够提升性能。

线程池的主要参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

线程池的主要参数有这6个:

corePoolSize:核心池的大小,核心池的线程不会被回收,没有任务就处于空闲状态。

maximumPoolSize:线程池最大允许的线程数,

keepAliveTime:当线程数超过corePoolSize时,但小于等于maximumPoolSize,会生成‘临时’线程,‘临时’线程执行完任务不会马上被回收,如果在keepAliveTime时间内,还没有新任务的话,才会被回收。

unit:keepAliveTime的单位。

workQueue:当前线程数超过corePoolSize大小时,新任务会处在等待状态,存在workQueue中。

threadFactory:生成新线程的工厂。

handler:当线程数超过workQueue的上限时,新线程会被拒绝,这个参数就是新线程被拒绝的方式。

其中corePoolSize和maximumPoolSize相对难理解,再详细说明:
1、池中线程数小于corePoolSize,新任务都不排队而是直接添加新线程

2、池中线程数大于等于corePoolSize,workQueue未满,首选将新任务加入workQueue而不是添加新线程

3、池中线程数大于等于corePoolSize,workQueue已满,但是线程数小于maximumPoolSize,添加新的线程来处理被添加的任务

4、池中线程数大于大于corePoolSize,workQueue已满,并且线程数大于等于maximumPoolSize,新任务被拒绝,使用handler处理被拒绝的任务

因此maximumPoolSize、corePoolSize不宜设置过大,否则会造成内存、cpu过载的问题,workQueue而尽量可以大一些。

Executors

虽然ThreadPoolExecutor使用很方便,但是建议大家使用jdk已经设定的几种线程池:
Executors.newCachedThreadPool()(无界线程池,可以进行线程自动回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor()(单个后线程),它们满足大部分的场景需求。

1.newSingleThreadExecutor 单线程线程池
看下它的实现方式:

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

把corePoolSize设为1,而workQueue大小设为无限大,因此永远只有一个线程在执行任务,新任务都在workQueue中等待。

2.newFixedThreadPool 固定大小线程池

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

和newSingleThreadExecutor 有些类似,只不过从单线程变成可以指定线程数量,workQueue依旧为无限。
3.newCachedThreadPool

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

newCachedThreadPool所有新任务都会被立即执行,corePoolSize 设置为0,maximumPoolSize设置为整数最大值,所有线程超过60秒未被使用,就会被销毁。

workQueue

当前线程数超过corePoolSize大小时,新任务会处在等待状态,存在workQueue中。workQueue的实现方式也就是任务的等待策略,分为3种:有限队列、无限队列、直接提交。

谈谈前2种,jdk使用了LinkedBlockingQueue而非ArrayBlockingQueue,有限队列的优势在于对资源和效率更定制化的配置,但是对比无限队列缺点也很明显:

1).增加开发难度。需要考虑到corePoolSize ,maximumPoolSize,workQueue,3个参数大小,既要使任务高效地执行,又要避免任务超量的问题,对于开发和测试的复杂度提高很多。
2).防止业务突刺。在互联网应用业务量暴增也是必须考虑的问题,在使用无限队列时,虽然执行可能慢一点,但是能保证执行到。使用有限队列,会出现任务拒绝的问题。
综上考虑,更建议使用无限队列,尤其是对于多线程不是很熟悉的朋友们。

拒绝策略

所谓拒绝策略之前也提到过了,任务太多,超过maximumPoolSize了怎么办?当然是接不下了,接不下那只有拒绝了。拒绝的时候可以指定拒绝策略,也就是一段处理程序。

决绝策略的父接口是RejectedExecutionHandler,JDK本身在ThreadPoolExecutor里给用户提供了四种拒绝策略,看一下:

1、AbortPolicy

直接抛出一个RejectedExecutionException,这也是JDK默认的拒绝策略

2、CallerRunsPolicy

尝试直接运行被拒绝的任务,如果线程池已经被关闭了,任务就被丢弃。

3、DiscardOldestPolicy

移除最晚的那个没有被处理的任务,然后执行被拒绝的任务。同样,如果线程池已经被关闭了,任务就被丢弃了

4、DiscardPolicy

悄悄地拒绝任务

    原文作者:java线程池
    原文地址: https://segmentfault.com/a/1190000013995322
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞