一、概述

1、为什么使用线程池

在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
  4. 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

2、线程池体系

java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等

多线程篇3:线程池_i++

Executor接口:这个接口也是整个线程池中最顶层的接口,提供了一个无返回值的提交任务的方法

public interface Executor {

//提交运行任务,参数为Runnable接口对象,无返回值
void execute(Runnable command);
}

由于这个接口过于简单,我们无法得知线程池的执行结果数据,如果我们不再使用线程池,也无法通过Executor接口来关闭线程 池。此时,我们就需要ExecutorService接口的支持了。 

ExecutorService接口:非定时任务类线程池的核心接口,通过ExecutorService接口能够向线程池中提交任务(支持有返回结果和无 返回结果两种方式)、关闭线程池、唤醒线程池中的任务等。ExecutorService接口的源码如下所示,这个接口也是我们在使用非定时任务类的线程池中最常使用的接口

public interface ExecutorService extends Executor {

//关闭线程池,线程池中不再接受新提交的任务,但是之前提交的任务继续运行,直到完成
void shutdown();

//关闭线程池,线程池中不再接受新提交的任务,会尝试停止线程池中正在执行的任务。
List<Runnable> shutdownNow();
//判断线程池是否已经关闭

boolean isShutdown();
//判断线程池中的所有任务是否结束,只有在调用shutdown或者shutdownNow方法之后调用此方法才会返回true。

boolean isTerminated();
//等待线程池中的所有任务执行结束,并设置超时时间

boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//提交一个Callable接口类型的任务,返回一个Future类型的结果

<T> Future<T> submit(Callable<T> task);

//提交一个Callable接口类型的任务,并且给定一个泛型类型的接收结果数据参数,返回一个Future类型的结果
<T> Future<T> submit(Runnable task, T result);
//提交一个Runnable接口类型的任务,返回一个Future类型的结果

Future<?> submit(Runnable task);
//批量提交任务并获得他们的future,Task列表与Future列表一一对应

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;

//批量提交任务并获得他们的future,并限定处理所有任务的时间
<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;
}

AbstractExecutorService抽象类:派生自ExecutorService接口,实现了几个非常实现的方法,供子类进行调用。

public abstract class AbstractExecutorService implements ExecutorService {


//RunnableFuture类用于获取执行结果,在实际使用时,我们经常使用的是它的子类FutureTask,newTaskFor方法的作用就是将任务封装成FutureTask对象,后续将FutureTask对象提交到线程池。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}


public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}


public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}


private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
//提交的任务为空,抛出空指针异常
if (tasks == null)
throw new NullPointerException();
//记录待执行的任务的剩余数量
int ntasks = tasks.size();
//任务集合中的数据为空,抛出非法参数异常
if (ntasks == 0)
throw new IllegalArgumentException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
//以当前实例对象作为参数构建ExecutorCompletionService对象
// ExecutorCompletionService负责执行任务,后面调用用poll返回第一个执行结果
ExecutorCompletionService<T> ecs =
new ExecutorCompletionService<T>(this);



try {
// 记录可能抛出的执行异常
ExecutionException ee = null;
// 初始化超时时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Iterator<? extends Callable<T>> it = tasks.iterator();

//提交任务,并将返回的结果数据添加到futures集合中
//提交一个任务主要是确保在进入循环之前开始一个任务
futures.add(ecs.submit(it.next()));
--ntasks;
//记录正在执行的任务数量
int active = 1;

for (;;) {
//从完成任务的BlockingQueue队列中获取并移除下一个将要完成的任务的结果。
//如果BlockingQueue队列中中的数据为空,则返回null
//这里的poll()方法是非阻塞方法
Future<T> f = ecs.poll();
//获取的结果为空
if (f == null) {
//集合中仍有未执行的任务数量
if (ntasks > 0) {
//未执行的任务数量减1

--ntasks;
//提交完成并将结果添加到futures集合中
futures.add(ecs.submit(it.next()));
//正在执行的任务数量加•1

++active;
}
//所有任务执行完成,并且返回了结果数据,则退出循环
//之所以处理active为0的情况,是因为poll()方法是非阻塞方法,可能导致未返回结果时active为0
else if (active == 0)
break;
//如果timed为true,则执行获取结果数据时设置超时时间,也就是超时获取结果表示
else if (timed) {
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
//没有设置超时,并且所有任务都被提交了,则一直阻塞,直到返回一个执行结果
else
f = ecs.take();
}
//获取到执行结果,则将正在执行的任务减1,从Future中获取结果并返回
if (f != null) {
--active;
try {
return f.get();
} catch (ExecutionException eex) {
ee = eex;
} catch (RuntimeException rex) {
ee = new ExecutionException(rex);
}
}
}

if (ee == null)
ee = new ExecutionException();
throw ee;

} finally {
//如果从所有执行的任务中获取到一个结果数据,则取消所有执行的任务,不再向下执行
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
try {
return doInvokeAny(tasks, false, 0);
} catch (TimeoutException cannotHappen) {
assert false;
return null;
}
}

public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return doInvokeAny(tasks, true, unit.toNanos(timeout));
}

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks) {
RunnableFuture<T> f = newTaskFor(t);
futures.add(f);
execute(f);
}
for (int i = 0, size = futures.size(); i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
try {
f.get();
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
}
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks)
futures.add(newTaskFor(t));

final long deadline = System.nanoTime() + nanos;
final int size = futures.size();

// Interleave time checks and calls to execute in case
// executor doesn't have any/much parallelism.
for (int i = 0; i < size; i++) {
execute((Runnable)futures.get(i));
nanos = deadline - System.nanoTime();
if (nanos <= 0L)
return futures;
}

for (int i = 0; i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
if (nanos <= 0L)
return futures;
try {
f.get(nanos, TimeUnit.NANOSECONDS);
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
} catch (TimeoutException toe) {
return futures;
}
nanos = deadline - System.nanoTime();
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}

}

这个方法是批量执行线程池的任务,最终返回一个结果数据的核心方法,通过源代码的分析,我们可以发现,这个方法只要获取到 一个结果数据,就会取消线程池中所有运行的任务,并将结果数据返回。这就好比是很多要进入一个居民小区一样,只要有一个人 有门禁卡,门卫就不再检查其他人是否有门禁卡,直接放行。 在上述代码中,我们看到提交任务使用的ExecutorCompletionService对象的submit方法,我们再来看下 ExecutorCompletionService类中的submit方法,如下所示。 

综上所述,在非定时任务类的线程池中提交任务时,本质上都是调用的Executor接口的execute方法

ScheduledExecutorService定时任务接口,派生自ExecutorService接口,拥有ExecutorService接口定义的全部方法,并扩展 了定时任务相关的方法

Executors线程池工具类,提供了几种快速创建线程池的方法

二、Executors

可以用Executors工具类根据不同场景创建对应的线程池

1、newSingleThreadExecutor

创建只有一个线程的线程池;

保证所有任务按照指 定顺序(先入先出或者优先级)执行;

如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它;

public void singleThreadExecutorDemo(){
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
final int index = i;

singleThreadExecutor.execute(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+", index="+index);
}
});

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

pool-1-thread-1, index=0
pool-1-thread-1, index=1
pool-1-thread-1, index=2

从运行结果可以看出,所有任务都是在单一线程运行的。

2、newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。线程池的大小上限为Integer.MAX_VALUE

public void cachedThreadPoolDemo(){
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int index = i;

cachedThreadPool.execute(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+", index="+index);
}
});

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

pool-1-thread-1, index=0
pool-1-thread-1, index=1
pool-1-thread-1, index=2
pool-1-thread-1, index=3
pool-1-thread-1, index=4

从运行结果可以看出,整个过程都在同一个线程pool-1-thread-1中运行,后面线程复用前面的线程。

3、newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,超出的线程会在队列中等待;如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

public void fixedThreadPoolDemo(){
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 6; i++) {
final int index = i;

fixedThreadPool.execute(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+", index="+index);
}
});

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

pool-1-thread-1, index=0
pool-1-thread-2, index=1
pool-1-thread-3, index=2
pool-1-thread-1, index=3
pool-1-thread-2, index=4
pool-1-thread-3, index=5

从运行结果可以看出,线程池大小为3,每休眠1s后将任务提交给线程池的各个线程轮番交错地执行。线程池的大小设置,可参数Runtime.getRuntime().availableProcessors()。

4、newScheduledThreadPool

创建一个定长的线程池,可定时执行或周期执行任务,该方法可指定线程池的核心线程个数

public void scheduledThreadPoolDemo(){
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
//定时执行一次的任务,延迟1s后执行
scheduledThreadPool.schedule(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+", delay 1s");
}
}, 1, TimeUnit.SECONDS);

//周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+", every 3s");
}
}, 2, 3, TimeUnit.SECONDS);
}

pool-1-thread-1, delay 1s
pool-1-thread-1, every 3s
pool-1-thread-2, every 3s
pool-1-thread-2, every 3s
  • schedule(Runnable command, long delay, TimeUnit unit),延迟一定时间后执行Runnable任务;
  • schedule(Callable callable, long delay, TimeUnit unit),延迟一定时间后执行Callable任务;
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),延迟一定时间后,以间隔period时间的频率周期性地执行任务;
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit),与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。

ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。

以上四种线程池,都是基于ThreadPoolExecutor创建的线程池,只是new ThreadPoolExecutor()的时候参数不同而已。

5、newSingleThreadScheduledExecutor

创建一个单线程化的线程池,支持定时、周期性的任务执行

6、newWorkStealingPool

JDK1.8新增线程池,一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。 newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现;由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。

三、ThreadPoolExecutor

1、参数

ThreadPoolExecutor类继承了AbstractExecutorService类,并提供了四个构造器,参数最多的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  • corePoolSize:(线程池基本大小)核心池的大小,在创建线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法来预创建线程。即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
  • maximumPoolSize:线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime:(线程存活保持时间)表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。即当线程池中的线程数大于corePoolSize时,如果一个线程空闲时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,知道线程池中的线程数为0
  • unit:参数keepAliveTime的时间单位,有7中取值,在TimeUnit类中有7中静态属性: TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
  • workQueue:(任务队列):用于传输和保存等待执行任务的阻塞队列,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一版来说阻塞队列有以几种
  1. SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
  2. LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
  3. ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
  • threadFactory:(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
  • hander:(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。有四种取值
  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
  2. ThreadPoolExecutor.DiscardPolicy:默默丢弃任务 不进行任何通
  3. ThreadPoolExecutor.DiscardOlddestPolicy:丢弃队列最前的任务,重新尝试执行任务(重复此过程
  4. ThreadPoolExecutor.CallerRunsPolicy:有调用线程处理该任务

2、手动创建线程池

所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;

而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;

SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:

多线程篇3:线程池_大数据_02

上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?建立自己的线程工厂类,灵活设置关键参数


//这里默认拒绝策略为AbortPolicy private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));


使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
 
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();

private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());

通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的


ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();


3、ThreadPoolExecutor提供的启动、停止、监控任务的方法

向线程池提交的任务有两种:Runnable和Callable,二者的区别如下:

  1. 方法签名不同,void Runnable.run(), V Callable.call() throws Exception
  2. 是否允许有返回值,Callable允许有返回值
  3. 是否允许抛出异常,Callable允许抛出异常。

三种提交任务的方式:

  1. Future<T> submit(Callable<T> task) 有返回结果
  2. void execute(Runnable command) 没有返回结果
  3. Future<?> submit(Runnable task) 虽然返回Future,但是其get()方法总是返回null

任务停止

  1. shutdown():关闭线程池,等待任务都执行完
  2. shutdownNow():立即关闭线程池,不等待任务执行完

监控任务

  1. getTaskCount():线程池已执行和未执行的任务总数
  2. getCompletedTaskCount():已完成的任务数量
  3. getPoolSize():线程池当前的线程数量
  4. getCorePoolSize():线程池核心线程数
  5. getActiveCount():当前线程池中正在执行任务的线程数量

4、Future和FutureTask

Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:

get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;

get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;

cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;

isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;

isCancelled():判断任务是否被取消;

下面来实际演示Future和FutureTask的用法:

public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> future = executorService.submit(new Task());
Integer integer = future.get();
System.out.println(integer);
executorService.shutdown();
}

static class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程开始计算");
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executorService.submit(futureTask);
Integer integer = futureTask.get();
System.out.println(integer);
executorService.shutdown();
}

static class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程开始计算");
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}

5、线程池实例的几种状态

Running:运行状态,能接收新提交的任务,并且也能处理阻塞队列中的任务

shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略,当线程池处于Running状态时, 调用shutdown()方法会使线程池进入该状态

Stop(shutdownNow): 不能接收新任务,也不能处理阻塞队列中已经保存的任务,会中断正在处理任务的线程,如果线程池处于Running或 Shutdown状态,调用shutdownNow()方法,会使线程池进入该状态

Tidying: 如果所有的任务都已经终止,有效线程数为0(阻塞队列为空,线程池中的工作线程数量为0),线程池就会进入该状 态

isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true,处于Tidying状态的线程池调用terminated()方法,会使用线程池进入该状态

6、执行流程

多线程篇3:线程池_数据_03

1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

(1、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。)

7、线程池为什么需要使用(阻塞)队列?

因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

8、线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。

(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
while (task != null || (task = getTask()) != null) {})。

不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

9、如何配置线程池

CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

10、线程池的优化

当前在JDK中默认使用的线程池 ThreadPoolExecutor,在具体使用场景中,有以下几个缺点

1.core线程一般不会timeOut

2.新任务提交时,如果工作线程数小于 coreSize,会自动先创建线程,即使当前工作线程已经空闲,这样会造成空闲线程浪费

3.设置的maxSize参数只有在队列满之后,才会生效,而默认情况下容器队列会很大(比如1000)

如一个coreSize为10,maxSize为100,队列长度为1000的线程池,在运行一段时间之后的效果会是以下2个效果:

1.系统空闲时,线程池中始终保持10个线程不变,有一部分线程在执行任务,另一部分线程一直wait中(即使设置allowCoreThreadTimeOut)

2.系统繁忙时,线程池中线程仍然为10个,但队列中有还没有执行的任务(不超过1000),存在任务堆积现象

本文将描述一下简单版本的线程池,参考于 Tomcat ThreadPoolExecutor, 实现以下3个目标

1.新任务提交时,如果有空闲线程,直接让空闲线程执行任务,而非创建新线程

2.如果coreSize满了,并且线程数没有超过maxSize,则优先创建线程,而不是放入队列

3.其它规则与ThreadPoolExecutor一致,如 timeOut机制

首先看一下ThreadPoolExecutor的执行逻辑, 其基本逻辑如下

1.如果线程数小于coreSize,直接创建新线程并执行(coreSize逻辑)

2.尝试放入队列

3.放入队列失败,则尝试创建新线程(maxSize逻辑)

而执行线程的任务执行逻辑,就是不断地从队列里面获取任务并执行,换言之,即如果有执行线程,直接往队列里面放任务,执行线程就会被通知到并直接执行任务

空闲线程优先

空闲线程优先在基本逻辑中,即如果线程数小于coreSize,但如果有空闲线程,就取消创建线程的逻辑. 在有空闲线程的情况下,直接将任务放入队列中,即达到任务执行的目的。

这里的逻辑即是直接调整默认的ThreadPoolExecutor逻辑,通过重载 execute(Runnable) 方法达到效果. 具体代码如下所示:


public void execute(Runnable command) { //此处优先处理有活跃线程的情况,避免在<coreSize时,直接创建线程 if(getActiveCount() < getPoolSize()) { if(pool1.offer(command)) { return; } } super.execute(command); }


coreSize满了优先创建线程

从之前的逻辑来看,如果放入队列失败,则尝试创建新线程。在这个时候,相应的coreSize肯定已经满了。那么,只需要处理一下逻辑,将其offer调整为false,即可以实现相应的目的。

这里的逻辑,即是重新定义一个BlockingDeque,重载相应的offer方法,相应的参考如下:


public boolean offer(Runnable o) { //这里的parent为ThreadPoolExecutor的引用 int poolSize = parent.getPoolSize(); int maxPoolSize = parent.getMaximumPoolSize(); //还没到最大值,先创建线程 if(poolSize < maxPoolSize) { return false; } //默认逻辑 return super.offer(o); }


11、其它相关

在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute

这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP

12、Springboot中使用线程池


13、线程池复用线程原理

1.线程池里执行的是任务,核心逻辑在ThreadPoolExecutor类的execute方法中,同时ThreadPoolExecutor中维护了HashSet<Worker> workers;

2.addWorker()方法来创建线程执行任务,如果是核心线程的任务,会赋值给Worker的firstTask属性;

3.Worker实现了Runnable,本质上也是任务,核心在run()方法里;

4.run()方法的执行核心runWorker(),自旋拿任务while (task != null || (task = getTask()) != null)),task是核心线程Worker的firstTask或者getTask();

5.getTask()的核心逻辑:

1.若当前工作线程数量大于核心线程数->说明此线程是非核心工作线程,通过poll()拿任务,未拿到任务即getTask()返回null,然后会在processWorkerExit(w, completedAbruptly)方法释放掉这个非核心工作线程的引用;

2.若当前工作线程数量小于核心线程数->说明此时线程是核心工作线程,通过take()拿任务

3.take()方式取任务,如果队列中没有任务了会调用await()阻塞当前线程,直到新任务到来,所以核心工作线程不会被回收; 当执行execute方法里的workQueue.offer(command)时会调用Condition.singal()方法唤醒一个之前阻塞的线程,这样核心线程即可复用

多线程篇3:线程池_大数据_04

14、等待线程池中所有任务执行完成 

public class MyTest {
public static void main(String[] args) throws InterruptedException {
List<Integer> list = new Vector<>();
ExecutorService executorService = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 1000; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-index="+index);
try {
Thread.sleep(1000);
list.add(index);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
//不在接收新的任务
executorService.shutdown();
// 等待所有线程执行完毕
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println("all element size====================================================="+list.size());
}
}

四、ForkJoinPool

1、Fork/Join任务

Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。

我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成:

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行:

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行:

┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘

这就是Fork/Join任务的原理,Fork/Join的运⾏流程⼤致如下所示

多线程篇3:线程池_数据_05

需要注意的是,图⾥的次级⼦任务可以⼀直分下去,⼀直分到⼦任务⾜够⼩为⽌

2、Fork/Join的具体实现

前⾯我们说Fork/Join框架简单来讲就是对任务的分割与⼦任务的合并,所以要实现 这个框架,先得有任务。在Fork/Join框架⾥提供了抽象类 ForkJoinTask 来实现任 务。

ForkJoinTask是⼀个类似普通线程的实体,但是⽐普通线程轻量得多。

fork()⽅法:使⽤线程池中的空闲线程异步提交任务

// 本⽂所有代码都引⾃Java 8
public final ForkJoinTask<V> fork() {
Thread t;
// ForkJoinWorkerThread是执⾏ForkJoinTask的专有线程,由ForkJoinPool管理
// 先判断当前线程是否是ForkJoin专有线程,如果是,则将任务push到当前线程所负责的队列⾥去
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
// 如果不是则将线程加⼊队列
// 没有显式创建ForkJoinPool的时候⾛这⾥,提交任务到默认的common线程池中
ForkJoinPool.common.externalPush(this);
return this;
}

其实fork()只做了⼀件事,那就是把任务推⼊当前⼯作线程的⼯作队列⾥。

join()⽅法:等待处理任务的线程处理完毕,获得返回值。

案例1:Fork/Join对大数据进行并行求和:

public class Main {
public static void main(String[] args) throws Exception {
// 创建2000个随机数组成的数组:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}

static Random random = new Random(0);

static long random() {
return random.nextInt(10000);
}
}

class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
long[] array;
int start;
int end;

SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢计算速度:
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}

Fork/Join框架在Java标准库中就有应用。Java标准库​​java.util.Arrays.parallelSort(array)​​可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。