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 ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
  1. public class CompleteServiceTest {  
  2. public static void main(String[] args) throws InterruptedException, ExecutionException {  
  3.     ExecutorService executorService = Executors.newFixedThreadPool(10);  
  4.     CompletionService<String> completionService = new ExecutorCompletionService<String>(executorService);  
  5. /** 
  6.      * 产生一个随机数,模拟不同的任务的处理时间不同 
  7.      */  
  8. for (int i = 0; i < 10; i++) {  
  9.         completionService.submit(new Callable<String>() {  
  10. public String call(){  
  11. int rnt = new Random().nextInt(5);  
  12. try {  
  13.                     Thread.sleep(rnt*1000);  
  14.                 } catch (InterruptedException e) {  
  15. // TODO Auto-generated catch block  
  16.                     e.printStackTrace();  
  17.                 }  
  18.                 System.out.println("run rnt = "+rnt);  
  19. return String.valueOf(rnt*1000);  
  20.             }  
  21.         });  
  22.     }  
  23. /** 
  24.      * 获取结果时,总是先拿到队列上已经存在的对象,这样不用依次等待结果 
  25.      * 显然效率更高 
  26.      */  
  27. for (int i = 0; i < 10; i++) {  
  28.         Future<String> future = completionService.take();  
  29.         System.out.println(future.get());  
  30.     }  
  31.     executorService.shutdown();  
  32. }  
  33. }  

  

      通过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中。

所以,先完成的必定先被取出。这样就减少了不必要的等待时间

 

  

posted on 2018-03-12 16:31  xue123  阅读(578)  评论(0编辑  收藏  举报