【线程池
为了节省系统在多线程并发时不断创建和销毁线程带来的额外开销,就需要引入线程池。线程池的基本功能就是进行线程的复用。当系统接受一
个提交的任务时,并不会着急去创建一个新的线程去执行这个任务,而是去线程池中查询是否有空闲的线程。
若有:直接使用这个线程。
若没有:根据配置的策略执行(有可能时创建一个新的线程,也有可能是阻塞该任务等待空闲线程)。
待任务结束之后,也不会销毁线程,而是放入线程池的空闲队列,等待下次使用。
【executor框架
为了能更好的控制多线程,jdk提供了一套executor框架。其中ThreadPoolExecutor表示一个线程池。Executors表示一个线程工厂,通过Executors可以取得一个特定功能的线程池。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
但是这些工厂方法最后都是使用的ThreadPoolExecutor这个类。
【ThreadPoolExecutor参数含义:
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用prestartAllCoreThreads()或者prestartCoreThread()方法。从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0。当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。
直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;
threadFactory:线程工厂,主要用来创建线程;
handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛 出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的 任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
【现在重点讲讲workQueue:
参数workQueue是指被提交但未执行的任务队列,他是一个BlockingQueue接口的对象,仅用于存放runnable对象。根据队列功能分类,在ThreadPoolExecutor构造参数中可以使用以下几种BlockingQueue:
1.直接提交队列:
SynchronousQueue对象提供。SynchronousQueue是一个特殊的BlockingQueue,SynchronousQueue没有容量,不保存任务他总是将任务提交给线程执行,如果没有空闲的线程就会尝试创建新的线程。如果线程数达到maximumPoolSize,则执行拒绝策略。因此使用SynchronousQueue通常会设置很大的maximumPoolSize,否则容易执行拒绝策略。
2.有界任务队列:
有界任务队列可以使用ArrayBlockingQueue实现,其构造函数必须带一个参数表示最大容量。当使用有界任务队列的时候:如果有新任务提交,若线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize则会将任务置于等待队列中,如果等待队列已满,在线程数小于maximumPoolSize时,会创建新的线程执行任务,否则执行拒绝策略。最终,除非系统非常繁忙,否则线程数将会维持在corePoolSize。
3.无界任务队列:
无界任务队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,否则无界任务队列不存在任务入队失败的情况。当有新任务提交的时候:如果系统线程小于corePoolSize,会创建新的线程执行任务,如果大于corePoolSize则不会增加新的线程,任务会进入等待队列,无界队列会持续增长,直到系统资源耗尽。
4.优先任务队列:
带有执行优先级的队列,通过PriorityBlockingQueue实现,可以控制任务执行顺序特殊的无界队列。无论有界还是无界队列,都是fifo的执行任务,但是优先级任务队列可以根据任务自身的优先级决定任务执行顺序,在确保了性能的同时,也能保证执行质量(高优先级任务先执行)。
【线程池如何复用线程
我们知道线程池会复用线程,但是它的内部逻辑是如何将一个Runnable对象赋值给Thread的呢?
1.线程池内部维护的不是Thread对象而是一个内部类Worker:
它继承了AbstractQueuedSynchronizer类,实现了一个非重入的锁。该锁会保护一个正在等待任务被执行的Worker不被interrupt操作打断。为什么不用ReentrantLock,要用非重入的锁?因为作者不想让这个Worker task在setCorePoolSize这种线程池控制方法调用时能重新获取到锁。当提交一个任务时,如果需要创建一个线程(何时需要在下一节中探讨)时,就调用线程工厂创建一个线程,同时将线程绑定到Worker工作队列中。需要说明的是,Worker队列构造的时候带着一个任务Runnable,因此Worker创建时总是绑定着一个待执行任务。换句话说,创建线程的前提是有必要创建线程,不会无缘无故创建一堆空闲线程等着任务。这是节省资源的一种方式。
2.线程重用:
线程重用的核心是,它把Thread.start()给屏蔽起来了(一定不要重复调用),然后它自己有一个Runnable.run(),循环在跑,跑的过程中不断检查我们是否有新加入的子Runnable对象,有就调一下我们的run(),其实就一个大run()把其它小run()#1,run()#2,...给串联起来了,基本原理就这么简单。