一、概述
Java是天生就支持并发的语言,支持并发意味着多线程,线程的频繁创建在高并发及大数据量是非常消耗资源的,因为java提供了线程池。在jdk1.5以前的版本中,线程池的使用是及其简陋的,但是在JDK1.5后,有了很大的改善。JDK1.5之后加入了java.util.concurrent包,java.util.concurrent包的加入给予开发人员开发并发程序以及解决并发问题很大的帮助。这篇文章主要介绍下并发包下的Executor接口,Executor接口虽然作为一个非常旧的接口(JDK1.5 2004年发布),但是很多程序员对于其中的一些原理还是不熟悉,因此写这篇文章来介绍下Executor接口,同时巩固下自己的知识。如果文章中有出现错误,欢迎大家指出。
二、Executors工厂类
对于数据库连接,我们经常听到数据库连接池这个概念。因为建立数据库连接时非常耗时的一个操作,其中涉及到网络IO的一些操作。因此就想出把连接通过一个连接池来管理。需要连接的话,就从连接池里取一个。当使用完了,就“关闭”连接,这不是正在意义上的关闭,只是把连接放回到我们的池里,供其他人在使用。所以对于线程,也有了线程池这个概念,其中的原理和数据库连接池是差不多的,因此java jdk中也提供了线程池的功能。
线程池的作用:线程池就是限制系统中使用线程的数量以及更好的使用线程
根据系统的运行情况,可以自动或手动设置线程数量,达到运行的最佳效果:配置少了,将影响系统的执行效率,配置多了,又会浪费系统的资源。用线程池配置数量,其他线程排队等候。当一个任务执行完毕后,就从队列中取一个新任务运行,如果没有新任务,那么这个线程将等待。如果来了一个新任务,但是没有空闲线程的话,那么把任务加入到等待队列中。
为什么要用线程池:
- 减少线程创建和销毁的次数,使线程可以多次复用
- 可以根据系统情况,调整线程的数量。防止创建过多的线程,消耗过多的内存(每个线程1M左右)
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
比较重要的几个类/接口:
类/接口 | 说明 |
---|---|
ExecutorService | 真正的线程池接口 |
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题接口 |
ThreadPoolExecutor | ExecutorService的默认实现 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现 |
ForkJoinPool | 继承自AbstractExecutorService类,主要用于实现工作窃取 |
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
1. newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool
public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池的最大值是Integer的最大值(2^31-1)。
4. newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
5. newWorkStealingPool
public static ExecutorService newWorkStealingPool(int parallelism);
public static ExecutorService newWorkStealingPool();
根据给定的/所有可用的并行等级,创建一个拥有足够的线程数目的线程池(ForkJoinPool)。或许会使用多重队列来降低冲突。并行的等级是和运行的最大线程数目相关。真实的线程数目或许会动态地增长和收缩。一个工作窃取的线程池对于提交的任务不能保证是顺序执行的。
6. 其它一些创建后不可改变配置的线程池
public static ExecutorService unconfigurableExecutorService(ExecutorService executor);
public static ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor);
内部使用 DelegatedExecutorService / DelegatedScheduledExecutorService 实现创建之后就无法修改配置的线程池。
实例
注:使用了java8的lambda表达式以及stream
1.newSingleThreadExecutor
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
IntStream.range(0, 5).forEach(i -> executor.execute(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("finished: " + threadName);
}));
try {
// close pool
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(!executor.isTerminated()) {
executor.shutdownNow();
}
}
}
}
输出结果:
finished: pool-1-thread-1
finished: pool-1-thread-1
finished: pool-1-thread-1
finished: pool-1-thread-1
finished: pool-1-thread-1
线程名都一样,说明是同一个线程
2.newFixedThreadPool
public class FixedThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
IntStream.range(0, 6).forEach(i -> executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
String threadName = Thread.currentThread().getName();
System.out.println("finished: " + threadName);
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
try {
// close pool
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(!executor.isTerminated()) {
executor.shutdownNow();
}
}
}
}
输出结果:
finished: pool-1-thread-3
finished: pool-1-thread-2
finished: pool-1-thread-1
finished: pool-1-thread-2
finished: pool-1-thread-3
finished: pool-1-thread-1
只创建了三个线程
3.newCachedThreadPool
public class CachedThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
IntStream.range(0, 6).forEach(i -> executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
String threadName = Thread.currentThread().getName();
System.out.println("finished: " + threadName);
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
try {
// close pool
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(!executor.isTerminated()) {
executor.shutdownNow();
}
}
}
}
输出结果:
finished: pool-1-thread-4
finished: pool-1-thread-6
finished: pool-1-thread-5
finished: pool-1-thread-3
finished: pool-1-thread-2
finished: pool-1-thread-1
创建了6个线程
4.newScheduledThreadPool
public class ScheduledThreadExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() ->
System.out.println(System.currentTimeMillis())
, 1000, 2000, TimeUnit.MILLISECONDS);
try {
// close pool
Thread.sleep(10000);
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(!executor.isTerminated()) {
executor.shutdownNow();
}
}
}
}
输出结果:
1533218364402
1533218366403
1533218368401
1533218370402
1533218372403
5. newWorkStealingPool
让我们通过一个简单的需求来使用下Fork/Join框架,需求是:计算1+2+3+4的结果。
使用Fork/Join框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。
因为是有结果的任务,所以必须继承RecursiveTask,实现代码如下:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = 5390823896306412900L;
private static final int THRESHOLD = 2; //阈值
private Integer start;
private Integer end;
public CountTask(Integer start, Integer end) {
super();
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
Integer sum = Integer.valueOf(0);
//如果任务足够小就计算任务
boolean canCompute = (end-start) <= THRESHOLD;
if(canCompute) {
for(int i=start; i<=end; i++) {
sum += i;
}
} else {
//如果任务大于阀值,就分裂成两个子任务计算
int middle = (start + end) / 2;
CountTask leftTask = new CountTask(start, middle);
CountTask rightTask = new CountTask(middle+1, end);
//执行子任务
leftTask.fork();
rightTask.fork();
//等待子任务执行完,并得到其结果
Integer leftResult = (Integer) leftTask.join();
Integer rightResult = (Integer) rightTask.join();
//合并子任务
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = (ForkJoinPool) Executors.newWorkStealingPool();
//生成一个计算任务,负责计算1+2+3+4+...+99+100
CountTask task = new CountTask(1, 100);
//执行一个任务
Future<Integer> result = forkJoinPool.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
输出结果:
5050
newWorkStealingPool 的例子引用自:Java并发编程指南15:Fork/join并发框架与工作窃取算法剖析