Java线程池

  1. 什么是线程池
           线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,对线程的生命周期进行管理,对现有的线程重复利用,并且能够以一种简单的方式将任务的提交与执行相解耦。
  2. 使用线程池的好处
    线程池能够带来三个好处。
    第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
  3. 线程池相关知识
    java中的线程池是通过Executor框架实现的,Executor的UML图:(常用的几个接口和子类)

    Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),
     
    ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返回Future的方法
     
    AbstractExecutorService:ExecutorService执行方法的默认实现
     
    ScheduledExecutorService:一个可定时调度任务的接口
     
    ScheduledThreadPoolExecutor:ScheduledExecutorService的实现,一个可定时调度任务的线程池
     
    ThreadPoolExecutor:线程池,可以通过调用Executors以下静态工厂方法来创建线程池并返回一个ExecutorService对象:
    常用线程池介绍:
    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);

    前三个方法都是调用第四个方法实现的 。


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) //后两个参数为可选参数

    参数说明:
    corePoolSize:核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务,即使线程池中的其他线程是空闲的
    maximumPoolSize:最大线程数,可允许创建的线程数,corePoolSize和maximumPoolSize设置的边界自动调整池大小:
    corePoolSize <运行的线程数< maximumPoolSize:仅当队列满时才创建新线程
    corePoolSize=运行的线程数= maximumPoolSize:创建固定大小的线程池
     
    keepAliveTime:如果线程数多于corePoolSize,则这些多余的线程的空闲时间超过keepAliveTime时将被回收
     
    unit:keepAliveTime参数的时间单位
     
    workQueue:保存任务的阻塞队列,与线程池的大小有关:
      当运行的线程数少于corePoolSize时,在有新任务时直接创建新线程来执行任务而无需再进队列
      当运行的线程数等于或多于corePoolSize,在有新任务添加时则选加入队列,不直接创建线程
      当队列满时,在有新任务时就创建新线程
     
    threadFactory:使用ThreadFactory创建新线程,默认使用defaultThreadFactory创建线程

    handle:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy,任务被拒绝时将抛出RejectExecutorException

    阻塞队列workQueue的类型为BlockingQueue<Runnable>
    1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    4. PriorityBlockingQueue:一个具有优先级得无限阻塞队列。

    线程工厂(ThreadFactory)

    线程池创建线程都是通过的ThreadFactoryThread newThread(Runnable r)方法来创建的。下面是Executors类里的默认线程工厂方法的源码。

        static class DefaultThreadFactory implements ThreadFactory {
            private static final AtomicInteger poolNumber = new AtomicInteger(1);
            private final ThreadGroup group;
            private final AtomicInteger threadNumber = new AtomicInteger(1);
            private final String namePrefix;
    
            DefaultThreadFactory() {
                SecurityManager s = System.getSecurityManager();
                group = (s != null) ? s.getThreadGroup() :
                                      Thread.currentThread().getThreadGroup();
                namePrefix = "pool-" +
                              poolNumber.getAndIncrement() +
                             "-thread-";
            }
    
            public Thread newThread(Runnable r) {
                Thread t = new Thread(group, r,
                                      namePrefix + threadNumber.getAndIncrement(),
                                      0);
                if (t.isDaemon())
                    t.setDaemon(false);
                if (t.getPriority() != Thread.NORM_PRIORITY)
                    t.setPriority(Thread.NORM_PRIORITY);
                return t;
            }
        }
    

    从上面可以看出默认线程工厂创建出的是一个非守护、优先级为Thread.NORM_PRIORITY 的线程。如果想要自己定制线程工厂满足需求,只需实现ThreadFactory接口的Thread newThread(Runnable r)方法。

    饱和策略(RejectedExecutionHandler)

    JDK中的ThreadPoolExecutor类提供了4种不同的RejectedExecutionHandler实现:

    • AbortPolicy默认的饱和策略,该策略抛出未检查(运行时异常)的RejectedExecutionException
    • DiscardPolicy 不执行任何操作,直接抛弃任务
    • CallerRunsPolicy 在调用者线程中执行该任务
    • DiscardOldestPolicy 丢弃阻塞队列中的第一个任务, 然后重新将该任务交给线程池执行

    同样的,可以通过实现RejectedExecutionHandler接口自定义饱和策略。

    线程池的关闭
    ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
    shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
    shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

    线程池流程图:

  4. 线程池的API
    可以使用已经有的Executors类来创建线程池,也可以自己配置ThreadPoolExecutor创建线程池。
    Executors 自带创建线程池的方法:

    1. newSingleThreadExecutor 单个线程 阻塞队列是无界的

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

    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    2.newFixedThreadPool 固定大小的线程池 阻塞队列无界

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

    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    3. newCachedThreadPool 可缓存的线程池

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

    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    SynchronousQueue是一个是缓冲区为1的阻塞队列。

    4.newScheduledThreadPool 核心线程池固定,大小无限的线程池

    public static ExecutorService newScheduledThreadPool(int corePoolSize) {         
        return new ScheduledThreadPool(corePoolSize, 
                  Integer.MAX_VALUE,                                                  
                  DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,                                                    
                  new DelayedWorkQueue());    
    }

    创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

  5. 合理配置线程池

    要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

    1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
    2. 任务的优先级:高,中和低。
    3. 任务的执行时间:长,中和短。
    4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

    任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

    优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

    执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

    依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

    建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

  6. 线程池监控

    通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
    taskCount:线程池需要执行的任务数量。
    completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
    largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
    getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
    getActiveCount:获取活动的线程数。

    通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值