线程池的好处&线程池框架Executor

线程池的好处?为什么使用线程池? 

         服务端的应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。现在服务器的CPU一般都是多核的,如果我们在面对巨大的请求时,还是以单线程的方式去处理这样的一个任务,显然它的耗时是我们所无法接受的,而且不能发挥我们服务器多核CPU的优势。

那么我们便会采用多线程的方式去处理这样的场景,我们很自然的能够想到使用new Thread()等方式,为每一个请求创建一个线程,这样就能发挥我们多核CPU的优势,并行处理这些任务。

以上的方式在一定程度可以满足需求,但是我们认为这样无限制创建线程存在不足

 1)线程生命周期的开销非常高。线程的创建并不是没有代价的。根据平台的不同,实际的开销也有所不同,但是线程的创建过程都会需要时间,延迟处理的请求,并且需要JVM和操作系统提供一些辅导操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算资源

2)资源消耗。活跃的线程会消耗资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将会闲置。大量的空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时还将产生其他的性能开销。如果你已经拥有足够多的线程使所有cpu保持忙碌状态,那么再创建更多的线程反而会降低性能。

3)稳定性。在可创建线程的数量上存在一个限制。这个限制将随着平台的不同而不同,并且受多个因素制约,包括jvm的启动参数、Thread构造函数中请求的栈大小,以及底层操作的限制等。如果破坏这些限制,那么很可能抛出OutOfMemoryError异常,要想从这种错误中恢复过来是非常危险的,更简单的办法是通过构造程序来避免超出这种限制。

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有以下好处

1、降低资源消耗,提高线程的重用。线程的重用也提高了响应速度;

2、控制线程池的并发数,不会因为无限制的创建线程引起资源不足;

3、提高线程的可管理性。线程池可以提供定时、定期、单线程、并发数控制等功能。

扫描二维码关注公众号,回复: 5639446 查看本文章

 

线程池框架Executor

Executor:执行者,java线程池框架的最上层父接口,地位类似于spring的BeanFactry、集合框架的Collection接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行。

ExecutorService:该接口继承自Executor接口,添加了shutdown、shutdownAll、submit、invokeAll等一系列对线程的操作方法,该接口比较重要,在使用线程池框架的时候,经常用到该接口。

AbstractExecutorService:这是一个抽象类,实现ExecuotrService接口,

ThreadPoolExecutor:这是Java线程池最核心的一个类,该类继承自AbstractExecutorService,主要功能是创建线程池,给任务分配线程资源,执行任务。

ScheduledExecutorSerivceScheduledThreadPoolExecutor 提供了另一种线程池:延迟执行和周期性执行的线程池。

Executors:这是一个静态工厂类,该类定义了一系列静态工厂方法,通过这些工厂方法可以返回各种不同的线程池。

线程池的使用也体现解耦的思想,将任务的创建,运行进行了分离。Executor基于生产者-消费者模式,提交任务相当于生产者(生成待完成的工作单元工作单元,包括Runnable和Callable),执行任务的线程则相当于消费者(执行这些工作单元,由Executor框架完成)。

 

ThreadPoolExecutor

(1)线程池的运行状态:

ThreadPoolExecutor是Java线程池的核心类,先简单了解一下线程池运行的状态。

在ThreadPoolExecutor中定义了一个Volatile变量,另外定义了几个static final变量表示线程池的各个状态:

1.	volatile int runState;
2.	static final int RUNNING    = 0;
3.	static final int SHUTDOWN   = 1;
4.	static final int STOP       = 2;
5.	static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

当创建线程池后,初始时,线程池处于RUNNING状态;

如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

(2)线程池的构造函数

ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造函数,前面三个构造函数都是调用的第四个构造函数进行的初始化工作。以下为第四个构造函数:

 public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue<Runnable> workQueue,

                              RejectedExecutionHandler handler)

1.corePoolSize(线程池的基本大小)

当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

2.runnableTaskQueue(任务队列)

用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

 ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

 LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue,是一个无界阻塞队列。

 SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。

 PriorityBlockingQueue:一个具有优先级得无限阻塞队列。

3.maximumPoolSize(线程池最大大小)

线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

4.ThreadFactory:用于设置创建线程的工厂

可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。

5.RejectedExecutionHandler(饱和策略)

当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

6.keepAliveTime(线程活动保持时间)

线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

7.TimeUnit(线程活动保持时间的单位)

可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

结合下图理解,线程池的运行过程:

(1)先判断线程池中的核心线程们是否空闲,如果空闲,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的核心线程数还小于 corePoolSize,那就再创建一个核心线程。

(2) 如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新来的任务放到等待队列中去。如果等待队列又满了,那么查看一下当前线程数是否到达maximumPoolSize,如果还未到达,就继续创建线程。

 (3)如果已经到达了,就交给RejectedExecutionHandler(拒绝策略)来决定怎么处理这个任务。

         JDK中源码实现如下所示:

    //有任务提交过来的话,会执行这个方法
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
         //这个是先做第一个判断当前线程是不是大于等于核心线程池(说明满了),如果大于会继续执行第        
         二步把提交过来的任务添加到任务队列中去
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        //如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;
            if (runState == RUNNING && workQueue.offer(command)) {
              //如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,
              //则说明需要启用备用球员来上场(maximumPoolSize可以把这个看成是紧急预备队),来去处
              理这个提交的任务
                if (runState != RUNNING || poolSize == 0)
                //然后去处理任务
                    ensureQueuedTaskHandled(command);

                }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

具体的源码实现细节可以参考:https://blog.csdn.net/qq_36520235/article/details/81539770

这篇博客对源码进行了一步一步分解介绍。

 

Executors

Executors中工厂类中最常见的四类具有不同特性的线程池分别为SingleThreadExecutor,FixThreadPool、CachedThreadPool、ScheduleThreadPool。这几个线程池都是Executors类的静态方法,可以直接调用。例如:Executors.newSingleThreadPool ().execute(r);

 

(1)SingleThreadExecutor

public static ExecutorService newSingleThreadPool (int nThreads){

    return new FinalizableDelegatedExecutorService ( new ThreadPoolExecutor (1, 1, 0, TimeUnit. MILLISECONDS, new LinkedBlockingQueue<Runnable>()) );

}

SingleThreadPool只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题,采用的是无界阻塞队列,队列没有限制大小,所有的新任务都会等待执行。

 

(2)FixThreadPool

public static ExecutorService newFixThreadPool(int nThreads){

    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

}

FixThreadPool只有核心线程,并且数量固定的,也不会被回收,所有线程都活动时,同样的是队列没有限制大小,新任务会等待执行。

 

(3)CachedThreadPool

public static ExecutorService newCachedThreadPool(int nThreads){

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit. SECONDS, new SynchronousQueue<Runnable>());

}

CachedThreadPool只有非核心线程,最大线程数非常大,整数的最大值,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务。

任务队列SynchronousQueue相当于一个空集合,导致任何任务都会被立即执行,比较适合执行大量的耗时较少的任务

 

以上的三个线程池核心都是通过ThreadPoolExecutor构造一个特定使用场景的线程池,比较好理解,区别仅在于核心线程数的设置,以及阻塞队列的选择上。

 

(4)ScheduleThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){

return new ScheduledThreadPoolExecutor(corePoolSize);

}

public ScheduledThreadPoolExecutor(int corePoolSize){

super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedQueue ());

}

//使用,延迟1秒执行,每隔2秒执行一次Runnable r

Executors. newScheduledThreadPool (5).scheduleAtFixedRate(r, 1000, 2000, TimeUnit.MILLISECONDS);

核心线程数固定,非核心线程(闲着没活干会被立即回收)数没有限制。从上面代码也可以看出,ScheduledThreadPool主要用于执行定时任务以及有固定周期的重复任务。

 

如何合理配置线程池的大小

一般需要根据任务的类型来配置线程池大小:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
  • 如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

 

参考资料:

  1. https://blog.csdn.net/qq_28325291/article/details/82859913
  2. https://blog.csdn.net/fengye454545/article/details/79536986
  3. https://blog.csdn.net/qq_35222843/article/details/79165210
  4. https://blog.csdn.net/travellersy/article/details/76921946
  5. http://youzhixueyuan.com/use-of-java-thread-pool.html
  6. https://blog.csdn.net/seu_calvin/article/details/52415337
  7. https://blog.csdn.net/he90227/article/details/52576452

 

猜你喜欢

转载自blog.csdn.net/ChenjCarryOn/article/details/88776213