对线程,线程池,及线程返回值的一点总结
1 线程的实现方式,无论怎么封装,只有三种,本质上就是两种实现方法,对run()方法的重写和对call()方法的重写,继承Thread类和实现Runnable接口都是对run()方法的重写,而实现Callable()接口则是对call()方法的重写
run()方法不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。不能声明抛出检查型异常则更麻烦一些。
(1)Callable规定的方法是call(),而Runnable规定的方法是run()。 (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。 (3)call()方法可抛出异常,而run()方法是不能抛出异常的。 (4)运行Callable任务可拿到一个Future对象。
Callable任务返回Future对象。即:Callable和Future一个产生结果,一个拿到结果。
Future 表示异步计算的结果。Future接口中有如下方法:
- boolean cancel(boolean mayInterruptIfRunning)
取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
- boolean isCancelled()
任务是否已经取消,任务正常完成前将其取消,则返回 true
- boolean isDone()
任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
- V get()
等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
- V get(long timeout, TimeUnit unit)
同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果。也可以设置任务执行的超时时间,这个设置超时的方法就是实现Java程序执行超时的关键。
2 线程的初始化,线程可以通过线程池初始化
Java通过Excutors提供四种线程池的实现,具体可以百度或查阅JDk文档,这里只讨论线程池初始化线程的方式
先初始化一个线程池,
ExecutorService mExecutor = Executors.newSingleThreadExecutor();(四种方式中的一种)
(1) 初始化一个Runnable()的线程
mExecutor.submit(new Runnable() {
@Override
public void run() {
fibc(20);
}
});
斐波那契数列的实现方法,后面都是用这个,不再一一都写
static int fibc(int num) {
if (num == 0) {
return 0;
}
if (num == 1) {
return 1;
}
return fibc(num – 1) + fibc(num – 2);
}
这个线程是没有返回值的,当然你可以用Future来接收,不过get()的也是null
如:
Future<?> future = mExecutor.submit(new Runnable() {
@Override
public void run() {
fibc(20);
}
});
future.get()值为null
(2) 初始化一个Callable()的线程,我们知道Callable()线程是带返回值的
Future<Integer> result2 = mExecutor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return fibc(20);
}
});
可以拿到返回值result2.get(),值为6765
(3) 用futureTask来初始化一个Callable()线程
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return fibc(20);
}
});
mExecutor.submit(futureTask);
这个线程的返回值是这样获取的futureTask.get(),值为6765
3 对多个线程返回值队列的处理
(1) 方式1 自己写集合来实现获取线程池中任务的返回结果
public void testByQueue() throws Exception {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
BlockingQueue<Future<String>> queue = new LinkedBlockingQueue<Future<String>>();
// 向里面扔任务
for (int i = 0; i < 5; i++) {
Future<String> future = pool.submit(new MyThread(“Thread” + i));
queue.add(future);
}
// 检查线程池任务执行结果
for (int i = 0; i < 5; i++) {
System.out.println(“method1:” + queue.take().get());
}
// 关闭线程池
pool.shutdown();
}
方法2 通过CompletionService来实现获取线程池中任务的返回结果
public void testByCompetion() throws Exception {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
CompletionService<String> cService = new ExecutorCompletionService<String>(pool);
// 向里面扔任务
for (int i = 0; i < TOTAL_TASK; i++) {
cService.submit(new MyThread(“Thread” + i));
}
// 检查线程池任务执行结果
for (int i = 0; i < TOTAL_TASK; i++) {
Future<String> future = cService.take();
System.out.println(“method2:” + future.get());
}
// 关闭线程池
pool.shutdown();
}
使用方法一,自己创建一个集合来保存Future存根并循环调用其返回结果的时候,主线程并不能保证首先获得的是最先完成任务的线程返回值。它只是按加入线程池的顺序返回。因为take方法是阻塞方法,后面的任务完成了,前面的任务却没有完成,主程序就那样等待在那儿,只到前面的完成了,它才知道原来后面的也完成了。
使用方法二,使用CompletionService来维护处理线程不的返回结果时,主线程总是能够拿到最先完成的任务的返回值,而不管它们加入线程池的顺序。
(2) 使用CompletionService
public interface CompletionService<V>
将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
- public class CompleteServiceTest {
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- ExecutorService executorService = Executors.newFixedThreadPool(10);
- CompletionService<String> completionService = new ExecutorCompletionService<String>(executorService);
- /**
- * 产生一个随机数,模拟不同的任务的处理时间不同
- */
- for (int i = 0; i < 10; i++) {
- completionService.submit(new Callable<String>() {
- public String call(){
- int rnt = new Random().nextInt(5);
- try {
- Thread.sleep(rnt*1000);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(“run rnt = “+rnt);
- return String.valueOf(rnt*1000);
- }
- });
- }
- /**
- * 获取结果时,总是先拿到队列上已经存在的对象,这样不用依次等待结果
- * 显然效率更高
- */
- for (int i = 0; i < 10; i++) {
- Future<String> future = completionService.take();
- System.out.println(future.get());
- }
- executorService.shutdown();
- }
- }
通过CompletionService包装ExecutorService,然后调用其take()方法去取Future对象。
以前没研究过这两者之间的区别。今天看了源代码之后就明白了。
ExecutorService 与 CompletionService 两者最主要的区别在于submit的task不一定是按照加入自己维护的list顺序完成的。
从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。
而CompletionService的实现是维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。
所以,先完成的必定先被取出。这样就减少了不必要的等待时间