java线程池原理讲解及常用创建方式

什么是线程池

其实线程池的概念和数据库链接池的概念类似。线程池的作用就是为了避免系统频繁地创建和销毁线程。在线程池中,总有几个活跃的线程,当你需要使用线程是,可以从池子中随便拿一个空闲线程,当完成工作是,并不是关闭线程,而是将这个线程退回到池子,方便其他人使用。

JDK对于线程池常用类的讲解

首先我们先看一下线程池的类图关系,只有理解了这些类的关系后,后面的理解就容易多了:
 
  • Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,该方法用于接收执行用户提交任务。  
  • ExecutorService 接口继承了Executor接口,定义了线程池终止和创建及提交 futureTask 任务支持的方法。并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等。
  • AbstractExecutorService 是抽象类,它实现了ExecutorService接口及其中的的所有方法。主要实现了 ExecutorService和 futureTask 相关的一些任务创建和提交的方法。
  • ThreadPoolExecutor 继承了类AbstractExecutorService,它是最核心的一个类,它的实例对象其实就代表了一个线程池,是线程池的内部实现。线程池的功能都在这里实现了,平时用的最多的基本就是这个。其源码很精练,远没当时想象的多。
  • ScheduledThreadPoolExecutor 在 ThreadPoolExecutor 的基础上提供了支持定时调度的功能。线程任务可以在一定延时后才被触发执行。
  • 最后我们充电说一下上面UML图中的Executors类,其实这个类扮演着线程池工厂的角色,它里面提供了几种构造不同线程池的方法:
 public static ExecutorService newFixedThreadPool(int nThreads)
 public static ExecutorService newSingleThreadExecutor()
 public static ExecutorService newCachedThreadPool()
 public static ScheduledExecutorService newSingleThreadScheduledExecutor()
 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
 

构造线程池的常用的几种方式

  • newFixedThreadPool()方法:创建语句是:ExecutorService pool = Executors.newScheduledThreadPool(10),该方式返回一个固定线程数量的线程池,这里为10。该线程池中的的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂时存放在一个队伍队列中,待有线程空闲时,便处理在任务队列的任务
  • newSingleThreadExecutor方法:创建语句是:ExecutorService pool = Executors.newSingleThreadExecutor(),该方式返回一个只有一个线程的线程池。若多于的任务被提交到该线程池,任务就会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • newCachedThreadPool()方法:创建语句是:ExecutorService pool = Executors.newCachedThreadPool(),该方式返回一个可根据实际情况调整线程数量的线程池,线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池中进行复用。
  • newSingleThreadScheduledExecutor()方法:创建语句:ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor(),该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService 接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务
  • newScheduledThreadPool()方法:创建语句:ScheduledExecutorService pool = Executors.newScheduledThreadPool(10),该方法也返回一个ScheduledExecutorService对象,但是可以给该线程池指定线程数量,这里指定为10。
 
下面举一个实例来看看具体的使用个,这里以newFixedThreadPool()为例:
public class ThreadPoolDemo {

	public static class MyTask implements Runnable{

		@Override
		public void run() {

			System.out.println(System.currentTimeMillis()+":Thread ID:"
			+Thread.currentThread().getId());
			try{
				Thread.sleep(1000);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		
	}
	public static void main(String[] args) {
		MyTask task = new MyTask();
		ExecutorService es = Executors.newFixedThreadPool(5);
		for(int i = 0;i<10;i++){
			es.execute(task);
		}
	}
}
上述代码中,我们创建了固定大小的线程池,内有5个线程。然后我们依次向线程池提交了10个任务。此后,线程池就会安排调度这10个任务。每个任务都会讲自己的执行时间和执行这个线程的ID打印出来,并且在这里,安排每个任务执行1秒钟。得到的结果下:
1504283763066:Thread ID:10
1504283763066:Thread ID:9
1504283763066:Thread ID:12
1504283763067:Thread ID:13
1504283763067:Thread ID:11
1504283764066:Thread ID:9
1504283764066:Thread ID:10
1504283764066:Thread ID:12
1504283764067:Thread ID:11
1504283764067:Thread ID:13
 

线程池的内部实现

首先我们先看newFixedThreadPool()和newCachedThreadPool()的内部实现,之所以选它们,因为它们具有代表性。
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
由以上线程池的实现代码可以看到,它们都只是ThreadPoolExecutor类的封装。接下来看看ThreadPoolExecutor最重要的构造数:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
函数的参数含义如下:
  • corePoolSize:指定了线程池中的线程数量
  • maximumPoolSize:制定了线程池中的最大线程数量。
  • keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间。
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可,也可以自己实现。
  • handler:拒绝策略。当任务太多来不及处理,如果拒绝任务。

workQueue参数

参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用来存放Runnable对象。接下来介绍几个主要的任务队列,它们都是实现了BlockingQueue
 
(1)直接提交的队列:该功能有SynchronousQueue对象提供。SynchronousQueue是一个特殊的BlockingQueue。SynchronousQueue没哟容量,每一个插入操作都要等待一个相应的删除操作,反之,也是一样。如果我们使用SynchronousQueue,提交的任务不会保存在SynchronousQueue中(这里SynchronousQueue的内部实现大家可以查阅一下资料,这里不详细说了),而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程数量已经达到最大值,执行拒绝策略。因此,使用SynchronousQueue队列,通常设置很大的maximumPoolSize值,否则很容易执行拒绝策略。
 
(2)有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现。因为它是基于数组实现的,这样我们初始化一个固定容量即可。当时用有界的任务队列时。若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的进程执行任务。若大于maximumPoolSize,则执行拒绝策略。
 
(3)无界任务队列:无界任务队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当新任务到来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。
 
说完workQueue参数再来看看newFixedThreadPool()和newCachedThreadPool()两个方法的内部实现,第一个方法返回一个corePoolSize和maximumPoolSize大小一样的,并且使用了LinkedBlockingQueue任务队列的线程池。第二个方法返回corePoolSize为0,maximumPoolSize无穷大的线程池,该线程池内无线程,而当任务被提交时,该线程池会使用空闲的线程执行任务,若无空闲线程,则将任务加入SynchronousQueue队列,而SynchronousQueue队列是一种直接提交的队列,它总会迫使线程池增加新的线程执行任务。对于newCachedThreadPool(),如果同时有大量任务被提交,而任务的执行又不那么快,那么系统便会开启大量的线程处理,这样做很快会耗尽系统资源。
 
ThreadPoolExecutor线程池的核心调度代码:
 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
当我们使用线程池执行任务时,即使用execute()方法调用任务时,ThreadPoolExecutor的调度源代码如上面所示,代码第5行的workerCountOf()函数取得当前线程的线程总数。当线程总数小于corePoolSize核心线程数是,会将任务通过addWorker()方法直接调度执行。否则,则在第10行代码workQueue.offer()进入等列。如果进入等待失败,则会执行第17行,将任务直接提交给线程池。如果当前线程数已经达到maximumPoolSize,则提交失败,执行最后一行代码。
逻辑图如下:

 

handler参数

ThreadPoolExecutor的这个参数制定了拒绝策略。也就是当任务数量超过系统实际承载能力时,ThreadPoolExecutor需要执行的拒绝策略。JDK默认有自己的默认拒绝策略。接下来我们实现RejectedExecutionHandler自定义一个自己的拒绝策略:
 
public class ThreadPoolDemo {

	public static class MyTask implements Runnable{

		@Override
		public void run() {

			System.out.println(System.currentTimeMillis()+":Thread ID:"
			+Thread.currentThread().getId());
			try{
				Thread.sleep(100);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		
	}
	public static void main(String[] args) throws InterruptedException {
		MyTask task = new MyTask();
		ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.SECONDS,
			new LinkedBlockingDeque<Runnable>(10),
			new RejectedExecutionHandler() {
					
				@Override
				public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
				{	System.out.println(r.toString()+"is discard");
				}
			});
		for(int i =0;i<Integer.MAX_VALUE;i++){
			es.execute(task);
			Thread.sleep(10);
		}
	}
}

上面代码定义了corePoolSize和maximumPoolSize都是5的线程池。如果理解线程池的执行原理的话,这段代码肯定会执行拒绝策略,上面代码使用了自定义线程池,使用的是自定义的拒绝策略,所以拒绝策略会按照我们自己实现的rejectedExecution()方法执行。下面我截取了部分结果:
1504289276848:Thread ID:9
1504289276858:Thread ID:10
demo.ThreadPoolDemo$MyTask@28d76d1eis discard
demo.ThreadPoolDemo$MyTask@28d76d1eis discard
1504289276889:Thread ID:11
1504289276892:Thread ID:13
 
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值