JAVA学习笔记之线程池

215 阅读5分钟

为什么使用线程池?

Thread是一个稀缺而昂贵的资源,会大量的消耗CPU,消耗内存,如果线程开启的过多,对操作系统是一个负担。线程分为创建,任务执行,销毁,使用线程池可以节省资源,缩短任务的执行时间。

线程池的使用

/**
 *类说明:线程池的使用
 */
public class UseThreadPool {
   //工作线程
    static class Worker implements Runnable
    {
        private String taskName;
        private Random r = new Random();

        public Worker(String taskName){
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }

        @Override
        public void run(){
            System.out.println(Thread.currentThread().getName()
                  +" Worker Runnable执行 the task : " + taskName);
            SleepTools.ms(r.nextInt(100)*5);
        }
    }
    
    static class CallWorker implements Callable<String>{
       
        private String taskName;
        private Random r = new Random();

        public CallWorker(String taskName){
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }      

      @Override
      public String call() throws Exception {
            System.out.println(Thread.currentThread().getName()
                  +" CallWorker CallAble 执行 the task : " + taskName);
            return Thread.currentThread().getName()+":"+r.nextInt(100)*5;
      }
       
    }

    public static void main(String[] args)
            throws InterruptedException, ExecutionException
    {
       ExecutorService pool = new ThreadPoolExecutor(2,4,3,TimeUnit.SECONDS,
             new ArrayBlockingQueue<Runnable>(10),
             new ThreadPoolExecutor.DiscardOldestPolicy());
        for(int i=0;i<6;i++) {
            Worker worker = new Worker("worker_"+i);
            pool.execute(worker);
        }
        for(int i=0;i<6;i++) {
            CallWorker callWorker = new CallWorker("callWorker_"+i);
            Future<String> result = pool.submit(callWorker);
            System.out.println(result.get());
        }
        pool.shutdown();
    }
}

代码可以看到

pool.execute(worker);//实现的接口是Runnable、

pool.submit(callWorker)//实现的接口是Callable,submit会返回一个结果,要返回结果,用submit

线程池得关闭:两种方式
shutdown:尝试关闭线程池,然后把当前没有执行任务的线程进行中断,一定成功 

shutdownNow:不管当前任务有没有执行,都会执行中断,不一定成功,因为线程的中断是一种协作机制,得看具体任务。

源码分析

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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

corePoolSize:核心线程数

maximumPoolSize:当前线程池可以使用的最大线程数

keepAliveTime:有的线程空闲,控制空闲线程的存活时间,超过这个时间进行销毁

TimeUnit:存活的时间单位

workQueue:提交的任务数大于线程数,放到阻塞队列里

threadFactory: 创建线程后,适当的做位调整工作

RejectedExecutionHandler: 任务太多,队列都放不下 ,进行拒绝策略

线程池的工作机制

截图.png

按上图所示,对线程池的工作原理总结

1、主线程将任务下发后,先放到corePool,核心线程里

2、继续下发任务,当核心线程满了之后,放到阻塞队列里BlockQueue里

3、主线程继续下发任务,当阻塞队列满了之后,放到最大线程数里,MaximumPool

4、当核心线程,阻塞队列,最大线程都满了之后,主线程再下发任务,就去调用拒绝策略

四种拒绝策略:

DiscardOldestPolicy:直接丢弃最老的AbortPolicy:直接抛出异常,默认配置这个

CallerRunsPolicy:让调用者线程去执行,谁往线程池提交任务,谁来执行这个任务

DiscardPolicy:最新提交的任务直接扔掉 自定义策略:实现

RejectedExecutionHandler,自定义再提交到线程池里

如何合理配置线程池,合理配置参数?

1、线程的大小 coolPoolSize ,maximumPoolSize,对线程的大小进行合理分配?

根据任务特性

Runtime.getRuntime().availableProcessors();//从操作系统去获取当前可用的CPU核心数

A、cpu密集型任务(cpu和内存频繁互动)。计算类型的操作,逻辑判断

针对CPU密集型任务,应该尽可能配置少的线程的线程池,如配置cpu+1个线程的线程池。

B、io密集型(频繁读写磁盘)任务和混合型任务。 网络通讯,读写磁盘,文件处理

针对IO密集型的任务,因为它不可能一直在执行IO操作,所以配置尽可能多的线程的线程池,如2CPU数量线程的线程池。

C、优先级不同的任务(高中低)。

混合型任务需要进行拆分,拆分为CPU密集型和IO密集型,如果这两个任务执行时间相差不大,拆分成两个线程池执行任务,如果两个任务的执行时间相差过大,则没必要进行拆分

2、阻塞队列的大小进行合理分配?

A、根据执行时间不同的任务(长中短)

执行时间不同的任务交给不同规模的线程池去执行,或者交给优先级队列去执行,让执行时间短的任务先执行。

B、是否依赖外部资源(如数据库链接)

优先级不同的任务交给优先级队列去执行,让优先级高的任务先执行

Attention: 如何区分是IO密集型还是CPU密集型? CPU主要用于计算,加密解密,逻辑类都是消耗CPU。 IO主要是文件的操作。网络数据的读写等。 网络通讯框架大部分使用的都是CPU核心数*2,根据不同的场景自己进行判断处理

拓展习题:

核心线程数为3,最大线程数为6,队列为10,这时候有1-20个任务,问执行任务的顺序是什么?

解答:插入顺序: 先核心线程数1-3,再队列 4-13,再最大线程数14,15,16.。满了之后 17 18 19 20 舍弃。

执行时候,先执行核心线程数 1,2 ,3 再执行最大线程数 14 15 16,最后执行队列 4 -13

故顺序为 1,2,3 ,14,15,16, 4,5,6,7,8,9,10,11,12,13 .

17 18 19 20 舍弃