如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。
为什么要线程池
多线程技术主要解决处理器单元内多个线程执行的问题,可以减少处理器单元的闲置时间,增加处理器单元吞吐能力,多线程的情况下确实可以最大限度发挥多核处理器的计算能能力,但是如果随意使用线程池,对系统性能反而不利
- 创建和销毁线程是需要时间:假如一个服务器完成一项任务所需要的时间为:T1表示线程创建时间,T2表示线程执行任务时间,T3表示销毁时间,如果T1+T3远大于T2会得不偿失
- 线程也需要占用内存空闲,大量的线程会抢占宝贵的内存资源,可能会导致OOM的异常
- 大量的线程回收也会给GC带来很大的压力,延长GC停顿的时间
- 大量的线程也会抢占CPU资源,CPU不停的在各个线程上下文切换,反而没有时间去处理线程运行的时候该处理的任务
什么是线程池
线程池就是实现创建若干的可执行的线程放入一个池中,需要的时候从池中获取线程,不用自行创建,使用完成不需要销毁线程而放入线程池中, 从而减少创建和销毁对象的开销
因此通过池资源来避免频繁的创建和销毁线程,让创建的线程进行复用,就有了线程池的概念,线程池里会维护一部分活跃的线程,如果有需要,就去线程池中取线程使用,用完归还到线程池中,免去创建和销毁线程的开销,且线程池也会对线程的数量有一定的限制,线程池的本质是对线程资源的复用。
线程池的优势:
1、降低资源的消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
2、提高响应速度,当任务到达时,任务可以不需要等待线程创建就可以立即执行
3、提高了线程的可管理性,不仅可以降低系统资源消耗,提高了系统稳定性
4、线程池可以实现统一的分配,调优和监控
线程池的架构
Executor接口是最基础的接口
ExecutorService接口继承了Executor,在其上添加了一些扩展方法,可以说是真正的线程池接口
AbstractExecutorService抽象类实现了ExecutorService中的大部分的接口
ThreadPoolExecutor继承了AbstractExecutorService,是线程池的具体实现
ScheduledExecutorService接口继承自ExecutorService接口,提供了"周期性执行"的功能
ScheduledThreadPoolExecutor类即继承自ThreadPoolExecutor由实现ScheduledExecutorService接口,是”带有周期性执行“功能的线程池
Executors是线程池的静态工厂,提供了快速创建线程池的静态方法
Executor接口:
提交任务接口,只提供了一个execute方法,执行Runable类型任务
public interface Executor {
void execute(Runnable command);
}
ExecutorService接口
public interface ExecutorService extends Executor {
//关闭线程池
void shutdown();
//立即关闭
List<Runnable> shutdownNow();
//是否关闭
boolean isShutdown();
//是否终止
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//提交任务 ,提交任务类型多样,还具有异步功能
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
ExecutorService接口是真正线程池的接口,在Executor基础上做了一些扩展,主要是提交任务,终止任务
其中Future模式是多线程设计常用的一种设计模式。Future模式可以理解成:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。 Future提供了三种功能:
- 判断任务是否完成
- 能够中断任务
- 能够获取任务执行的结果
向线程池中提交任务的submit方法不是阻塞方法,而Future.get方法是一个阻塞方法,当submit提交多个任务时,只有所有任务都完成后,才能使用get按照任务的提交顺序得到返回结果,所以一般需要使用future.isDone先判断任务是否全部执行完成,完成后再使用future.get得到结果。?
Future接口定义了主要的5个接口方法,有RunnableFuture和SchedualFuture继承这个接口,以及CompleteFuture和ForkJoinTask继承这个接口。
?ScheduledExecutorService接口
ScheduledExecutorService接口提供了具有周期性执行任务的方法,其继承了ExecutorService接口。
public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
Executors创建常见的线程池
Executor工厂类提供线程初始化接口,主要有如下几种
newFixedThreadPool-固定数量的线程池
public class testFixedThreadPool {
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
//固定数量的线程池
ExecutorService newThread = Executors.newFixedThreadPool(3);
for (int i = 0; i < 20; i++) {
newThread.submit(new Runnable() {
@Override
public void run() {
Random random = new Random();
System.out.println("线程:"+Thread.currentThread().getName()+",执行任务序号:"+num.getAndIncrement());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
?固定线程数量的线程池实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
?包括核心线程、最大线程、等待时间、时间单位以及阻塞队列中的LinkedBlockingQueue。
创建一个指定工作线程数的线程池,其中corepoolsize和maxnumPoolSize相等,阻塞队列是基于LinkedBlockingQueue实现的
FixedThreadPool:传入的核心线程数是固定的,所有称为有界线程池,最大线程数和核心线程数相等。
假设核心线程是3,一次性提交20个任务,先启动了3个线程执行3个任务,剩下17个任务进入阻塞队列,因为核心线程和最大线程数相等,所以keepAlivetime参数没有意义,等待任一线程执行结束就会继续从等待队列中获取一个任务进行执行,其优点在于固定线程数量的线程池可以提供程序效率和节省创建线程所消耗的时间开销。
newCachedThreadPool-可缓存工作线程线程池
public class testCachedThreadPool {
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
//可缓存工作线程的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
cachedThreadPool.submit(new Runnable() {
@Override
public void run() {
Random random = new Random();
System.out.println("线程:"+Thread.currentThread().getName()+",执行任务序号:"+num.getAndIncrement());
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
?具体实现:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
创建一个可缓存的工作线程池,核心线程是0,线程池中线程数可达Integer.MAX_VALUE,即2^32,线程默认存活时间60秒,内部使用的队列SynchronousQueue同步阻塞队列,在没有任务执行时,当线程空闲时间超过60秒,则工作线程将会终止,当提交新任务是,如果没有空闲线程,则创建新线程执行任务可缓存线程至,如果有新的任务且没有可用的空闲线程,则新建线程,如果长时间线程空闲(60s)则对线程进行回收,
此线程池不会线程池大小做线程,线程池大小完全依赖于操作系统能够创建的最大线程大小。
可缓存线程池应用场景适合耗时短,不需要考虑同步的场合
?newSingleThreadExecutor:单个线程的线程池
public class testSingleThreadExecutor {
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
//固定数量的线程池
ExecutorService newThread = Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) {
newThread.submit(new Runnable() {
@Override
public void run() {
Random random = new Random();
System.out.println("线程:"+Thread.currentThread().getName()+",执行任务序号:"+num.getAndIncrement());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
?底层实现
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单线程线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的一个线程可以保证提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列
newFixedThreadPool(1):固定数量的线程的线程池在给定参数为1的情况下就可以看做是newSingleThreadExecutor的同等实现
newScheduledThreadPool:周期性执行任务线程池
public class DIYRunnable implements Runnable{
private Integer num;
public DIYRunnable(Integer num) {
this.num = num;
}
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName()+":执行任务编号:"+num+":当前时间:"+System.currentTimeMillis());
}
}
public class testSchedule {
public static void main(String[] args) {
//周期性执行任务的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
for(int i = 0; i < 3; i++) {
scheduledExecutorService.scheduleWithFixedDelay(
new DIYRunnable(i),2,2,TimeUnit.SECONDS);
}
}
}
?底层实现:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
阿里手册说到线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
ScheduledThreadPoolExecutor:周期性线程池
/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期
* 也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
* 如果任何执行的任务超过了周期,随后的执行会延时,不会并发执行
*/
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command,
//初始延时
long initialDelay,
//间隔时间
long period,
//时间单位
TimeUnit unit);
/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
*/
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
scheduleAtFixedRate:该方法在initialDelay时长后第一次执行任务,以后每隔period时长,再次执行任务。注意,period是从任务开始执行算起的。开始执行任务后,定时器每隔period时长检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务
scheduleWithFixDelay:该方法在initialDelay时长后第一次执行任务,以后每当任务执行完成后,等待delay时长,再次执行任务
|