美文网首页
Java并发学习(六)----线程池学习笔记

Java并发学习(六)----线程池学习笔记

作者: 彳亍口巴 | 来源:发表于2020-03-26 20:20 被阅读0次

    前言

    线程池,顾名思义就是线程的池子,在每次需要取线程去执行任务的时候,没必要每次都创建新线程执行,线程池就是起着维护线程的作用,当有任务的时候就取出一个线程执行,如果任务执行完成则把线程放回到池子中,完成线程的复用,没必要每次都去创建和销毁现场,提高效率。
    线程池为线程的生命周期的开销和资源不足提供的解决方案。通过对多个任务的重用线程
    那么什么时候使用多线程呢?

    • 单个任务处理时间比较短
    • 处理的任务数比较大,创建和销毁线程消耗资源过多

    线程池的优势

    (1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
    (2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
    (3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
    (4)提供更强大的功能,延时定时线程池。


    1、Exector 接口

    • Exector 接口:运行新任务的简单接口
    • ExectorService 接口:扩展了Exector接口,添加了一些用来管理执行器生命周期和任务生命周期的方法
    • ScheduledExecutorService 接口:扩展自ExectorService接口,支持Future和定期执行任务
    • Exectors 类:包装了具体的几个常用的线程池的定义,便于使用

    注意到默认由executors创建的线程池的工作队列都是基于链表的阻塞队列,没有具体长度

    public class Executors {
        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
        public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>(),
                                          threadFactory);
        }
        
        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
        
        public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>(),
                                        threadFactory));
        }
        
        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
        
    
        public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>(),
                                          threadFactory);
        }
    
    

    2、ThreadPoolExecutor 类

    java的线程都是在JUC包中,其中java.uitl.concurrent.ThreadPoolExecutor是最为核心的类
    其包中包含的四种创建线程池的方法

    public class ThreadPoolExecutor extends AbstractExecutorService {
        .....
        public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                BlockingQueue<Runnable> workQueue);
     
        public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
     
        public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
     
        public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
        ...
    }
    

    具体的调用方法又可以是在Exectors类中,只是提供更加便利的线程池调用方法,如果希望使用更加灵活自定义的线程池,还是建议使用ThreadPoolExecutor


    2.1 参数说明

    • private volatile int corePoolSize;:核心池大小

    当当前线程数小于核心池数字,则新加一个任务就创建一个线程处理,当超过了核心池大小则加入到任务队列中,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了
    prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中

    • private volatile int maximumPoolSize;:最大线程数

    线程池最大能创建的线程数目

    • private volatile long keepAliveTime;:线程没有需要处理的任务时最多保持多久会终止

    默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,

    • keepAliveTime参数也会起作用,直到线程池中的线程数为0

    什么时候会出现运行线程数大于核心线程数呢?如果使用了默认的Executors类提供的默认的线程池生产方法,则生成的任务队列都是链表阻塞队列,都是没有长度限制的,那么这样也就暗示了使用链表阻塞队列的线程池,线程池个数最多是核心线程数个,列表阻塞队列才有可能出现填充满了的情况,才进而出现实际线程个数超过核心线程数的情况

    • private volatile ThreadFactory threadFactory;:创建线程的工厂bean

    其中Executors.defaultThreadFactory()提供默认的线程工厂bean

    • private final BlockingQueue<Runnable> workQueue;:任务队列
    • private volatile RejectedExecutionHandler handler:拒绝处理任务时的策略

    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务


    2.2 重要方法

    • execute()

    Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

    • submit()

    ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法

    • shutdown()

    关闭线程池的方法,不会接受新的任务,会等到已经存在的任务执行完成

    • shutdownNow()

    关闭线程池的方法,不会接受新的任务,已经运行的任务也会被中断执行
    注意!当线程处于关闭、关闭中的状态时,不会再接收新的任务,接收到的任务也是会被直接拒绝的,后续由拒绝Policy执行调用


    3、线程池流程

    image

    1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
    2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
    3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

    (1、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
    2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
    3、判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。)

    4、线程池为什么需要使用(阻塞)队列?

    回到了非线程池缺点中的第3点:
    1、因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

    另外回到了非线程池缺点中的第1点:
    2、创建线程池的消耗较高。
    或者下面这个网上并不高明的回答:
    2、线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。

    5、线程池为什么要使用阻塞队列而不使用非阻塞队列?

    阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
    当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
    使得在线程不至于一直占用cpu资源。

    (线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
    while (task != null || (task = getTask()) != null) {})。

    不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

    6、如何配置线程池

    CPU密集型任务
    尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

    IO密集型任务
    可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

    混合型任务
    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
    因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
    因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

    7、java中提供的线程池

    Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

    Java线程池对比

    1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

    2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

    3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

    4、newScheduledThreadPool:适用于执行延时或者周期性任务。


    8、使用示例

    public class Test {
        public static void main(String[] args) {  
            //核心线程数5,最大线程数10,阻塞队列采用ArrayBlockingQueue,做多排队5个
            ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<Runnable>(5));
             
            for(int i=0;i<15;i++){
                MyTask myTask = new MyTask(i);
                executor.execute(myTask);
                System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
                executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
            }
            executor.shutdown();
        }
    }
    
    
    class MyTask implements Runnable {
       private int taskNum;
        
       public MyTask(int num) {
           this.taskNum = num;
       }
        
       @Override
       public void run() {
           System.out.println("正在执行task "+taskNum);
           try {
               Thread.currentThread().sleep(4000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("task "+taskNum+"执行完毕");
       }
    

    运行结果:其中的一种结果

    正在执行task 0
    线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
    线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
    线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
    正在执行task 1
    线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
    正在执行task 2
    线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
    线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
    线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
    线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
    线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
    正在执行task 3
    正在执行task 4
    线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    正在执行task 10
    线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    正在执行task 11
    线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    正在执行task 12
    线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    正在执行task 13
    线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
    正在执行task 14
    task 0执行完毕
    task 1执行完毕
    task 2执行完毕
    正在执行task 5
    task 4执行完毕
    正在执行task 8
    正在执行task 6
    正在执行task 7
    task 3执行完毕
    正在执行task 9
    task 10执行完毕
    task 13执行完毕
    task 12执行完毕
    task 11执行完毕
    task 14执行完毕
    task 5执行完毕
    task 8执行完毕
    task 6执行完毕
    task 7执行完毕
    task 9执行完毕


    通过案例总结:
    当线程数小于核心线程数(5)时会创建新线程,如果要执行的线程大于5,就先把任务放入队列中,如果队列最大容量5已经满了,那会在创建线程,直到最大达到最大线程数10。
    并不能保证任务按顺序执行,通过结果也可以看出,任务的完成顺序并不是一定的

    注意
    这里如果创建超过15个,比如将for循环中改成执行20个任务,就会抛出任务拒绝异常了。因为你的队列和最大线程数才15,如果有20个任务就会抛异常。


    9、用线程池和不用线程池的区别是什么?

    public class ThreadCondition implements Runnable {
    
    
    @Test
    public void testThreadPool(){
       Runtime run=Runtime.getRuntime();//当前程序运行对象
        run.gc();//调用垃圾回收机制,减少内存误差
        Long freememroy=run.freeMemory();//获取当前空闲内存
        Long protime=System.currentTimeMillis();
        for(int i=0;i<10000;i++){
          new Thread(new ThreadCondition()).start();
        }
        System.out.println("独立创建"+10000+"个线程需要的内存空间"+(freememroy-run.freeMemory()));
        System.out.println("独立创建"+10000+"个线程需要的系统时间"+(System.currentTimeMillis()-protime));
    
    
        System.out.println("---------------------------------");
        Runtime run2=Runtime.getRuntime();//当前程序运行对象
        run2.gc();//调用垃圾回收机制,减少内存误差
        Long freememroy2=run.freeMemory();//获取当前空闲内存
        Long protime2=System.currentTimeMillis();
       ExecutorService service=Executors.newFixedThreadPool(2);
        for(int i=0;i<10000;i++){
         service.execute(new ThreadCondition()) ;
        } 
        System.out.println("线程池创建"+10000+"个线程需要的内存空间"+(freememroy2-run.freeMemory()));
        service.shutdown();
       
        System.out.println("线程池创建"+10000+"个线程需要的系统时间"+(System.currentTimeMillis()-protime2));
    
    
    
    
    }
    
    @Override
    public void run() {
    //null
    }
    
    }
    

    运行结果:


    这也就说明了,线程池的优势。


    引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)

    java多线程(5)---ThreadPoolExecutor
    Java8 线程池源码学习
    Java线程池详解

    相关文章

      网友评论

          本文标题:Java并发学习(六)----线程池学习笔记

          本文链接:https://www.haomeiwen.com/subject/kmvruhtx.html