Java并发编程系列:线程池原理详解

1. 为什么需要线程池

线程是轻量级的进程,但其创建和关闭人需要话费时间,如果每一个小任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程处理任务所需的时间。因此合理的创建和使用线程至关重要。然而线程池能做到这点。

和连接池类似,线程池中有创建好的线程,当需要时直接从池子中拿空闲的线程。当完成工作室,把线程放回线程池中。

2. JDK对线程池的支持

在java.util.concurrent包下,其中ThreadPoolExecutor表示一个线程池,Executors则表示一个线程池工厂的角色,通过Executors可以获得一个特定功能的线程池。由图可知,ThreadPoolExecutor实现了Executor,因此通过这个接口任何Runnable的对象都可以被ThreadPoolExecutor线程池调度。
线程池

Executors提供了几种创建线程池的方法:

static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。

static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个线程池(ScheduledExecutorService)对象,他在ExecutorService接口上扩展了在给定时间执行某任务的功能,在某个固定的延时之后执行。

static ExecutorService newSingleThreadExecutor()
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程,若多余的一个任务被提交到该线程池中,任务会保存在一个任务队列中,待线程空闲,按先入先出顺序执行队列中的任务。

static ExecutorService newCachedThreadPool()
该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲的线程可以复用,则会优先使用可复用的线程。若所有的线程均在工作,又有新的任务提交。则会创建新的线程处理任务。所有的线程在当前任务执行完毕后,将返回线程池进行复用。

2.1. 固定大小线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixPool extends Thread {
	@Override
	public void run(){
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getId());
	}
	public static void main(String args[]){
		Thread t = new FixPool();
		//创建一个固定大小的线程池,内有2个线程。
		ExecutorService es = Executors.newFixedThreadPool(2);
		for(int i=0;i<20;i++){
			//System.out.print(i);
			es.submit(t);
		}
	}
}

代码的结构大致为每个一秒,2个线程打印出id。
当把newFixedThreadPool改为newCachedThreadPool则结果就截然不同。

2.2. 计划任务线程池

通过newScheduledThreadPool()方法,它返回一个ScheduledExecutorService对象,可以根据时间需求对线程进行调度。
与其他线程池不同,ScheduledExecutorService并不会立即安排执行任务,他其实是计划任务的作用。

主要的方法:
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)创建并执行在给定延迟后启用的一次性操作。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。

解释:对于FixRate来说,任务调度频率是一定的,它是以上一个任务开始执行时间为起点,在之后的period时间后,调度下一次任务。而Fixdelay则是以上一个任务结束后,在经过delay时间进行任务调度。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TestSchedual {
	public static void main(String args[]){
		ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
		ses.scheduleAtFixedRate(new Runnable() {
			public void run() {
				try {
					Thread.sleep(4000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(System.currentTimeMillis()/1000);
			}
		}, 0, 2, TimeUnit.SECONDS);
	}
}

当程序执行时间比周期长是,那么任务就会在上一个任务结束后,立即被调用。
结果:
1501637712
1501637716
1501637720
1501637724

3.线程池内部实现

newFixedThreadPool、newScheduledThreadPool、newCachedThreadPool方法都是返回ThreadPoolExecutor的实例,只是进行了不同的包装。

ThreadPoolExecutor的构造函数:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, l
ong keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, 
ThreadFactory threadFactory, RejectedExecutionHandler handler)

参数的含义为:
corePoolSize:指定了线程池中的线程数量
maximumPoolSize:指定线程池中最大的数量
keepAliveTime:当前线程池线程数量超过orePoolSize时,多余的空闲线程的存活时间。
unit:时间单位
workQueue:任务队列,提交但未被执行的任务
threadFactory:线程工厂,用于创建线程,一般默认即可。
handler:拒绝策略,当前任务太多来不及处理,如何拒绝任务。

workQueue指被提交但未被执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable的对象。根据功能BlockingQueue可以分为以下几种:

3.1 直接提交队列

由SynchroizedQueue类实现,它是一个特殊的BlockingQueue,SynchroizedQueue没有容量,没插入一个操作都要等待相应的删除操作。

3.2 有界任务队列

有界的任务队列由ArrayBlockingQueue类实现,它的构造函数必须带一个容量参数,表示队列的最大容量。当使用有界任务队列时,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列,若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。否则,执行拒绝策略。总结:当任务队列装满时,才可能会将线程数执行到corePoolSize以上。

3.3 无界的任务队列

它是通过LinkedBlockingQueue类实现,当有新的任务带来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务。当系统的线程达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待。

3.4 优先任务队列

优先任务队列时待有执行优先级的队列,通过PriorityBlockingQueue类实现,可以控制任务的执行先后顺序。而有界队列和无界队列都是按照先进先出处理任务的,而PriorityBlockingQueue可根据自身的优先级顺序执行。

再来讨论那几个方法获取ExecutorService的源码细节:
在newFixedThreadPool中:

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

它返回一个corePoolSize和maximumPoolSize同样大小,并且使用了LinkedBolockingQueue任务队列的线程池。因为是固定的大小的线程池,所以不存在数量的变化。所以两者设置相同。它用无界队列存放任务,当任务提交非常频繁时,该队列可能迅速膨胀,从而耗尽资源。
newSingleThreadExecutor:

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

它返回了一个corePoolSize和maximumPoolSize为1,并且使用了LinkedBlockingQueue任务队列的线程池。与newFixedThreadPool基本类似.

newCachedThreadPool:

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

该方法返回corePoolSize为0,maximumPoolSize无穷大的线程池,意味着没有任务时,该线程池内无线程,当任务被提交时,该线程池会使用空闲的线程,若无空闲线程,则是将任务家务SynchroizedQueue任务队列,而SynchroizedQueue是一种直接提交的队列。他总会迫使线程池增加新的线程执行任务。任务执行完毕后,由于corePoolSize为0,因此空闲线程又会在之地动时间内(60秒)回收。对于newCachedThreadPool,如果有大量任务被提交,而执行任务的速度有不快,那么系统便会开启等量的线程处理,这样的做法很快将系统资源耗尽。

注意:使用线程池时要根据具体的情况,选择合适的并发队列,作为任务的缓冲,当资源紧张时,不同的并发队列对系统行为和性能影响不同。

4. 拒绝策略

拒绝策略是说当系统超负荷运行时的不就措施。即线程池中的线程用完了,同时等待队列也排满了。这时就需要一套机制,那就是拒绝策略。
JDK提供了四种拒绝策略:

  • AbortPolicy策略: 用于被拒绝任务的处理程序,它将抛出 RejectedExecutionException.
  • CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。但是任务提交线程的性能急剧下降。
  • DiscardOldestPolicy策略:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。
  • DiscardPolicy策略:该策略默默的将丢弃无法处理的任务,不予任何处理。

以上四种策略均是实现了RejectedExecutionHandler接口的类,如果以上策略任然无法满足实际应用,则可以自定义拒绝策略,步骤为:
实现RejectedExecutionHandler接口,实现未实现的方法。

public interface RejectedExecutionHandler{
	// 当 execute 不能接受某个任务时,可以由 ThreadPoolExecutor 调用的方法。
	void rejectedExecution(Runnable r, ThreadPoolExecutor executor) ;
}
其中r为请求执行的任务,executor为当前的线程池。
一个例子
import java.util.concurrent.*;
public class TestRej{
	public static class Run1 implements Runnable{
		public void run(){
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(System.currentTimeMillis()+Thread.currentThread().getId());
		}
	}
	public static void main(String aer[]) throws InterruptedException{
		ExecutorService es = new ThreadPoolExecutor(5, 5,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>(10),new RejectedExecutionHandler(){
							@Override
							public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
								System.out.println(r.toString()+" 被抛弃了。");
							}
		});
		Runnable run = new Run1();
		while(true){
			es.submit(run);
			Thread.sleep(10);
		}
	}
}

这里每一个任务需要执行50秒,每过10秒就有一个任务提交,线程池的容量为5,所以这里基本不会用到拒绝策略。

5.扩展线程池

ThreadPoolExecutor.Worker是一个内部类,它是一个实现了Runnable接口的类,ThreadPoolExecutor线程池中的工作线程也是Worker的实例,Worker.runWorker()方法会被线程池以多线程模式异步调用。runWorker()会被多个线程访问

try {
    beforeExecute(wt, task);
    Throwable thrown = null;
    try {
        task.run();
    } catch (RuntimeException x) {
        thrown = x; throw x;
    } catch (Error x) {
        thrown = x; throw x;
    } catch (Throwable x) {
        thrown = x; throw new Error(x);
    } finally {
        afterExecute(task, thrown);
    }
} finally {
    task = null;
    w.completedTasks++;
    w.unlock();
}

默认的ThreadPoolExecutor实现中,提供了空的beforeExecutor()和afterExecutor实现,在实际应用中,对其进行扩展来实现对线程池状态的跟踪,输出一些有的调试信息。有利于故障排查。

我们只需用自己的方法覆盖掉beforeExecutor()和afterExecutor即可。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值