多线程虽然能够提升程序的性能,但其实也是一把双刃剑。"为每一个任务分配一个线程"的问题在于资源管理的复杂性。当我们需要频繁的创建多个线程进行耗时操作时,每次通过new Thread来创建并不是一种好的办法。new Thread 新建和销毁对象的性能较差,线程缺乏统一的管理,而且可能出现无限制的创建线程。
每当看到这种形式的代码时:new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略,请考虑使用Executor来替代Thread。(参考《Java并发编程实战》)
因此,我们可以通过使用线程池来管理线程。线程池是指管理一组同构工作线程的资源池。其原理简单解释就是会创建多个线程并进行管理,提交给线程的任务会被线程池指派给其中的线程进行执行。
java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分,Executor接口如下:
public interface Executor {
void execute(Runnable command);
}
虽然
Executor是个简单的接口,但它缺位灵活且强大的异步任务执行框架提供了基础。
线程池都实现了ExecutorService接口,该接口定义了线程池需要实现的方法:
public interface ExecutorService extends Executor {
void shutdown();
List shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, Ti
meUnit unit)
throws InterruptedException;
Future submit(Callable task);
//.......(代码省略其他用于任务提交的便利方法)
}
ExecutorService接口的实现有,ThreadPoolExecutor和ScheduledThreadPoolExcecutor。
ThreadPoolExecutor继承自抽象类AbstractExecutorService,该抽象类实现了ExecutorService接口。ThreadPoolExecutor也是我们运用最多的线程池。
ScheduledThreadPoolExcecutor 继承了ThreadPoolExecutor 并实现了ScheduledExecutorService接口,ScheduledExecutorService接口继承自ExecutorService接口。ScheduledThreadPoolExcecutor用于周期性的执行任务,和Timer类类似。但Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExcecutor来代替它(Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExcecutor只支持基于相对时间的调度)。
我们可以通过ThreadPoolExecutor的构造函数来实例化一个对象,但由于创建参数相对复杂,通常选择Exectors工厂类的静态方法来创建一个线程池:
newFixedThreadPool。将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程池,直到达到线程池的最大数量,这时线程池的规模将不再变化。
newCachedThreadPool。将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
newSingleThreadPool。是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadPool能确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。
newScheduledThreadPool。创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
newFixedThreadPool和newCachedThreadPool 返回通用的ThreadPoolExecutor实例,这些实例可以直接用来构造专门用途的executor。
当默认的创建线程池策略无法满足要求时,那么可以通过ThreadPoolExecutor构造函数来实例化一个对象,根据自己的需求来实现定制,ThreadPoolExecutor最常见的构造函数形式如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
corePoolSize:线程池中所保存的核心线程数。线程池启动后默认是空的,只有任务来临时才会创建线程以处理请求。
maximumPoolSize:线程池允许创建的最大线程数。它与corePoolSize的作用是调整“线程池中实际运行的线程的数量”。当新任务提交给线程池时,如果线程池中运行的线程数量小于corePoolSize,则创建新线程来执行任务;如果此时,线程池中运行的线程数量大于corePoolSize,但小于maximumPoolSize,则仅当阻塞队列满时才创建新线程。如果corePoolSize与maximumPoolSize相同,则创建固定大小的线程池。如果将maximumPoolSize设置为基本的无界值(如Integer.MAX_VALUE),则允许线程池适应任意数量的并发任务。
keepAliveTime:当前线程池线程总数达到核心线程数时,终止多余的空闲线程的时间。
Unit:keepAliveTime参数的时间单位,可选值有毫秒、秒、分等。
workQueue:任务队列。如果当前线程池达到核心线程数量corePoolSize后,且当前所有线程都处于活动状态时,则将新加入的任务放到此队列中。
threadFactory:线程工厂,让用户可以定制线程的创建过程,一般不需要设置。
Handler:拒绝策略,当线程池和任务队列workQueue都满了的情况下,对新加的任务采取的处理策略。
下面通过两个简单的例子来说明线程池的简单使用:
1.通过Executors.newFixedThreadPool(int)来创建一个固定数量的线程池,代码如下:
public class MyExecutorDemo {
//执行的任务数量
private static int MAX = 10;
public static void main(String args[]){
try {
fixedThreadPool(4);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private static void fixedThreadPool (int coreSize)
throws InterruptedException,ExecutionException {
//创建线程池
ExecutorService exec = Executors.newFixedThreadPool(coreSize);
for(int i = 0; i < MAX; i++){
//提交任务
Future task = exec.submit(new Callable() {
@Override
public Integer call() throws Exception {
System.out.println("执行线程" + Thread.currentThread().getName());
return fibc(20);
}
});
//获取执行结果
System.out.println("第"+i+"次计算,结果为"+task.get());
}
}
//模拟耗时操作,定义一个斐波那契数列
private static int fibc(int num){
if (num == 0){
return 0;
}
if (num == 1){
return 1;
}
return fibc(num-1)+fibc(num-2);
}
}
在上述代码中,通过fixedThreadPool启动了含有4个线程的线程池,我们看一下该方法的构造函数,
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
第一个和第二个参数时相同的值,则创建固定大小的线程池,最后的参数为无界队列,因此该线程池可容纳无限个任务。创建成功后,向线程池中通过submit提交了10个Callable任务,每个任务计算前20个的斐波那契数列。通过结果可以看出,线程池中有4个线程交替的执行任务,执行运算结果如下:
执行线程pool-1-thread-1
第0次计算,结果为6765
执行线程pool-1-thread-2
第1次计算,结果为6765
执行线程pool-1-thread-3
第2次计算,结果为6765
执行线程pool-1-thread-4
第3次计算,结果为6765
执行线程pool-1-thread-1
第4次计算,结果为6765
执行线程pool-1-thread-2
第5次计算,结果为6765
执行线程pool-1-thread-3
第6次计算,结果为6765
执行线程pool-1-thread-4
第7次计算,结果为6765
执行线程pool-1-thread-1
第8次计算,结果为6765
执行线程pool-1-thread-2
第9次计算,结果为6765
newCachedThreadPool
2.通过Executors.newCacheThreadPool()创建带缓存的线程池
有时,我们需要任务尽可能快的被执行,这需要线程池中的线程足够多,也就是说需要用空间来换时间,创建的线程越多,占用的内存消耗也越大。但由于其并发量也越大,因此执行的速度也越快。考虑这样一种场景,在新提交了一个任务后,由于当前没有空闲线程可执行,因此需要马上创建一个线程来执行该任务。这种场景可通过该方法来实现。代码如下:
private static void newCachedThreadPool () throws ExecutionException,
InterruptedException{
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < MAX; i++){
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行线程为:"+Thread.currentThread().getName()+
",结果为:"+fibc(30));
}
});
}
}
执行线程为:pool-1-thread-2,结果为:832040
执行线程为:pool-1-thread-1,结果为:832040
执行线程为:pool-1-thread-3,结果为:832040
执行线程为:pool-1-thread-4,结果为:832040
执行线程为:pool-1-thread-1,结果为:832040
执行线程为:pool-1-thread-5,结果为:832040
执行线程为:pool-1-thread-2,结果为:832040
执行线程为:pool-1-thread-6,结果为:832040
执行线程为:pool-1-thread-1,结果为:832040
执行线程为:pool-1-thread-5,结果为:832040
为了保证执行效率,每次提交任务,线程池都会创建一个新线程来执行任务,但前提是此时没有空闲线程才创建新线程,但有空闲线程时,则会使用空闲的线程来执行任务,如结果中所示,执行前4个任务时,线程池为每个任务都创建了一个线程,当执行到第5个任务时,此时第一个线程已经执行完任务,并处于空闲状态,那么第5个任务就被执行在第1个线程中了。
(参考书籍《Java并发编程实战》、《Android开发进阶从小工到专家》)