线程池是程序设计领域池化技术的一种应用(数据库连接池也是一个典型的池化技术),池化技术解决了大量的短请求带来的系统频繁创建对象对性能的影响。我们可以实现自己的线程池,但往往因为考虑不周全如容错性、自动扩容与缩容等导致性能不佳!Java5.0 内置了对线程池的支持,提供了性能比较优越的线程池相关的类!我们就来简单介绍一下如何使用这个线程池!
【ExecutorService & Executors】
在Java5.0中,接口Executor代表一个任务执行器,我们往这个执行器中提交任务,这个执行器负责执行,方式可以为“当前提交线程执行”,“另起线程执行”或“线程池执行”,由具体不同实现决定。这样设计,Java将线程控制相关的操作封装在这里面。ExecutorService是这个接口的子接口,代表“线程池执行”这种方式!同时也提供了一些实现类,但我们不用自己去创建实现类对象,Executors提供了大量工厂方法去为我们做这个,我们先看一个例子:
package cn.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest1 {
public static void main(String[] args) {
// 创建线程数量为固定5个的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 向线程池提交任务即可
for(int i=0; i<5; i++){
threadPool.execute(new MyInnerTask());
}
}
static class MyInnerTask implements Runnable {
@Override
public void run() {
int i = 0;
while(i<10){
System.out.println(Thread.currentThread().getName() + " 执行第 " + i + " 任务循环!" );
i++;
}
}
}
}
部分输出为:
pool-1-thread-1 执行第 3 任务循环!
pool-1-thread-3 执行第 7 任务循环!
pool-1-thread-5 执行第 6 任务循环!
pool-1-thread-5 执行第 7 任务循环!
pool-1-thread-5 执行第 8 任务循环!
pool-1-thread-4 执行第 8 任务循环!
pool-1-thread-4 执行第 9 任务循环!
pool-1-thread-2 执行第 4 任务循环!
pool-1-thread-5 执行第 9 任务循环!
pool-1-thread-2 执行第 5 任务循环!
pool-1-thread-3 执行第 8 任务循环!
pool-1-thread-1 执行第 4 任务循环!
pool-1-thread-3 执行第 9 任务循环!
pool-1-thread-2 执行第 6 任务循环!
pool-1-thread-1 执行第 5 任务循环!
上面是创建了一个固定数量为5的线程池,然后我们就往其中提交任务即可,提交后,线程池就会执行。我们看到这个多线程例子中却没有任何线程相关的代码,所有这些代码都被封装到了ExecutorService中!固定数量的线程池就是线程池中总是维持固定数量的线程,没有任务了,线程数量不变,任务多了,线程数量也不变。这个适用的场景是:任务数量比较固定,且任务执行时间较长。
我们再看一种创建线程池的工厂方法:
ExecutorService threadPool = Executors.newCachedThreadPool();
这会创建一个有弹性的线程池,当任务较多并且此时所有线程都在执行任务,则会创建额外线程去执行!没有任务时,当线程的空闲时长达到60秒,则将线程杀死!这种线程池适合的场景是:任务数量不定,并且任务执行时间较短!
我们再看一个十分特别的线程池:
ExecutorService threadPool = Executors.newSingleThreadExecutor();
这个线程池中只会有一个线程,那这个和单线程有啥区别呢?首先线程创建不由我们去处理,其次线程执行因异常退出后,这个线程池会再起一个线程,即自动维持一个线程的存在!这种线程池适合的场景就是:固定需要一个线程去执行某种任务!
ExecutorService接口还有一个子接口ScheduledExecutorService,这是一种特殊的线程池,用于延迟执行任务或定期定时执行任务(可以认为是定时器池),我们同样是通过Executors的工厂方法去创建这种线程池,我们直接看用法了:
package cn.test;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest2 {
public static void main(String[] args) {
// 创建线程数量为固定5个的线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
// 向线程池提交任务即可
for(int i = 0; i<2; i++){
scheduledThreadPool.schedule(new MyInnerTask(), 5, TimeUnit.SECONDS);
}
// 主线程给出计时
for(int i=1; i<=5; i++){
System.out.println("等待 " + i + " 秒!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyInnerTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " come !!!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
看结果:
等待 1 秒!
等待 2 秒!
等待 3 秒!
等待 4 秒!
等待 5 秒!
pool-1-thread-1 come !!!
pool-1-thread-2 come !!!
上例,我们创建一个定时执行任务的线程池,含有两个固定的线程,主线程主要负责计时!调用线程池的schedule方法向其中部署任务。第二个参数5表示提交的任务会延时5秒后被执行!
ScheduledExecutorService类型的线程池在提交任务时,还有其他重载方法可以使用:
scheduledThreadPool.scheduleAtFixedRate(new MyInnerTask(), 5, 2, TimeUnit.SECONDS);
上述提交任务的方法,第一个5表示该任务延迟5秒被执行,第二个2表示,这个任务每隔2秒会被重新执行一次(具体的操作应该是该任务上的已被执行标示会在2秒后被清除)。这里有个原则是:如果任务本身的执行时长>2秒,则该任务会在被执行完毕后,才可以再次被执行(立即)!同一个任务不会出现多线程并发执行的情况!
另一个重载形式为:
scheduledThreadPool.scheduleWithFixedDelay(new MyInnerTask(), 5, 2, TimeUnit.SECONDS);
上述提交任务的方法,数字参数5和2的意思和上面的方法一致,但和上面方法有所区别的是:无论该任务执行多长时间,任务执行后,会再延迟2秒才可以被执行!即如果任务执行本身需要3秒钟,则该任务第一次执行和第二次可以被执行的间隔为5秒(被执行的3+延迟的2)。
【Callable & Futrue】
以前我们提到的所有线程相关的操作,线程执行完任务后都不会有返回值也不会抛出异常(Thread的run方法本身就没有返回值和异常声名)。有时,为了处理这种问题,我们不得不写很多额外代码!在并发包中,Java通过提供接口Callable来对这种情况进行了语言级的支持!Callable接口类似于Runnable接口,代表一段可以被线程去执行的代码!任务实现这个接口后,就可以让线程在执行过程中抛出异常或在执行完毕后返回结果。接口Future代表任务执行后的返回结果。我们先看个使用例子:
package cn.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableTest1 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
/**
* 创建只有一个线程的线程池,目前只能通过线程池来提交Callable类型的任务
*/
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 向线程池提交任务,并且得到Future类型返回值
Future future = threadPool.submit(new MyTask());
// 调用future的get方法,主线程进入阻塞状态,等待线程结果返回,如果线程执行抛异常,这里也会将异常抛出!
System.out.println(future.get());
}
static class MyTask implements Callable{
@Override
public String call() throws Exception {
// 睡眠3秒,代表执行某段复杂业务逻辑
Thread.sleep(3000);
String result = "下订单成功!";
return result;
}
}
}
目前只能向线程池中提交Callable类型的任务,提交该任务后,会立即返回一个Futrue对象,调用Future的get方法,即可得到线程的返回值或异常信息!注意get()方法是一个无限等待的阻塞调用的方法,直到线程返回正常结果或执行抛异常返回!Future还提供一个重载的get:
future.get(1, TimeUnit.SECONDS);
调用这个get,表明线程会在这里阻塞等待1秒钟,如果线程没有返回或抛出异常,则get方法会抛出java.util.concurrent.TimeoutException 的超时异常!
上例中,我们只是向线程池中提交了一个任务,并且向线程所要返回值。如果我们要提交多个需要返回值的任务,我们该如何做?一个做法就是for循环提交任务,将所有的future先保存在一个列表中,提交完毕后,主线程循环列表,逐个调用Future的get方法!但这种序列化处理方法无法优先处理排在后面的那些很快就返回结果的任务!并发包中对这种情况也给予了支持!
CompletionService接口用于提交一组Callable任务,其有一个默认实现类为:ExecutorCompletionService,我们看一下具体用法:
package cn.test;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CompletionServiceTest1 {
public static void main(String[] args) {
// 创建具有固定两个线程的线程池!
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 创建CompletionService,需要传递一个线程池,具体任务的执行还是由这个线程池去执行
CompletionService completionService = new ExecutorCompletionService(threadPool);
completionService.submit(new MyLongTask());
completionService.submit(new MyShortTask());
try {
Future future = null;
while((future = completionService.take()) != null){
System.out.println(future.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 需要长时间执行的任务
static class MyLongTask implements Callable{
@Override
public String call() throws Exception {
// 睡眠6秒,代表执行某段复杂业务逻辑
Thread.sleep(6000);
String result = "大大大大大订单下单成功!";
return result;
}
}
// 短时间就可执行完毕的任务
static class MyShortTask implements Callable{
@Override
public String call() throws Exception {
// 睡眠3秒,代表执行某段复杂业务逻辑
Thread.sleep(3000);
String result = "小小小订单下单成功!";
return result;
}
}
}
执行结果为:
小小小订单下单成功!
大大大大大订单下单成功!
我们看,小任务执行反比后先返回,这个结果就被先被处理了!
在创建ExecutorCompletionService对象completionService时,需要一个线程池的参数,也就是说,completionService在执行任务时,使用的还是线程池!这个对象本身就是去处理线程执行的返回结果。
completionService调用方法take,会阻塞,直到有一个Callable任务执行完毕返回,这个方法就会返回Future对象!completionService还提供一个poll方法,这个方法调用时,如果有Callable任务执行完毕,就返回其Future对象,否则会直接返回null!
使用ExecutorCompletionService提交多个Callable任务,最后会按照返回顺序,挨个进行处理!先执行完毕的任务会被先处理掉!
Callable、Future、CompletionService、ExecutorCompletionService几个类都使用了泛型,泛型即代表了线程的返回结果类型!