第八章 玩转线程池

8.1 在任务与执行策略之间的隐性耦合

Executor框架可以将任务的提交与任务的执行策略解耦开来。虽然Executor框架为制定和修改执行策略都提供了相当大的灵活性,但并非所有的任务都能使用所有的执行策略。

有些类型的任务需要明确地指定执行策略,包括:

①依赖性任务

如果提交给线程池的任务需要依赖其他的任务,那么就隐含地给执行策略带来了约束,此时必须小心地维持这些执行策略以避免产生活跃性问题

②使用线程封闭机制的任务

与线程池相比,单线程的Executor能能够对并发性做出更强的承诺。它们能确保任务不会并发地执行,使你能够放宽代码对线程安全的要求。对象可以封闭在任务线程中,使得在该线程中执行的任务在访问该对象时不需要同步,即使这些资源不是线程安全的。这种情形将在任务与执行策略之间形成隐式的耦合——任务要求其执行所在的Executor是单线程的(只要确保任务不会并发执行,并提供足够的并发机制,使得一个任务对内存的作用对于下一个任务一定是可见的——这正式newSingleThreadExecutor提供的保证)。如果将Executor从单线程环境改为线程池环境,那么将失去线程安全性。

③对响应时间敏感的任务

如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性。

④使用ThreadLocal的任务

ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。然而只要条件允许,Executor可以自由地重用这些线程。
在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛出了一个未检查异常,那么将用一个新的工作者线程来代替抛出异常的线程。只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务之间传递值。

只有当任务都是同类型的并且互相独立时,线程池的性能才能达到最佳。
如果将运行时间较长和运行时间较短的任务混合在一起,除非线程池很大,否则将可能造成“拥塞”
如果提交的任务依赖于其他任务,除非线程池无限大,否则可能造成死锁。

在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖与其他的任务,那么会要求线程池足够大,从而确保它们的依赖任务不会内放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行侧路而破坏了安全性或活跃性。

8.1.1 线程饥饿死锁

在线程中,如果任务依赖与其他任务,那么可能产生死锁。
在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务完成,而第一个任务又无法完成,因为它在等待第二个任务的完成。

在更大的线程池中,如果所有正在执行的任务的线程都由于等待其他仍处于工作队列的任务而阻塞,那么会发生同样的问题,这个现象被称为线程饥饿死锁(Thread Starvation Deadlock)。

只要线程池中的任务需要无限期等待一些必须由线程池中其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

在下面的例子中给出了线程饥饿死锁的示例。RenderPageTask向Executor提交了两个任务来获取网页的页眉和页脚。绘制页面,等待获取页眉和页脚任务的结果,然后将页脚,页面主题和页眉组合起来并形成最终的页面。当使用单线程的Executor,那么ThreadDeadlock会经常发生死锁。同样,如果线程池不够大,那么当多个任务通过栅栏机制来彼此协调时,将导致线程饥饿死锁。

//  8-1      在单线程Executor中任务发生死锁(不要这样做)
public class ThreadDeadlock {
       ExecutorService exec=Executors.newSingleThreadExecutor();

       public class RenderPageTask implements Callable<String>{
           public String call() throws Exception{
               Future<String> header,footer;
               header=exec.submit(new LoadFileTask("header.html"));
               footer=exec.submit(new LoadFileTask("footer.html"));
               String page=renderBody();  //render,递交
               //将发生死锁——由于任务在等待子任务的结果
               return header.get()+page+footer.get();
           }
       }
}

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程饥饿死锁,因此需要在代码或者配置Executor的配置文件中记录线程池的大小限制或配置限制。

8.1.2 运行时间较长的任务

执行时间较长的任务不仅会造成线程池堵塞,还会增加执行时间较短任务的服务时间。如果线程池中线程的数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。

限定任务等待资源的时间而不是无限制地等待,可以缓解执行时间较长任务造成的影响。
在类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本(例如Thread.join,BlockingQueue.put,CountDownLatch.awati以及Selector.select等)。

如果执行超时,那么可把任务标识为失败,然后中止任务或将任务重新返回队列中以便随后执行。
如果在线程池中总是充满了被阻塞的任务,那么也可能表名线程池的规模过小。

8.2设置线程池的大小

线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors来动态计算。

如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么就应该考虑使用多个线程池,从而使每个线程池可以根据各自的工作负载来调整。

在计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为Ncpu+1,通常能实现最优的利用率。

对于包含I/O操作或其他阻塞操作的任务,由于线程不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。

还可以通过另外一种方法来调节线程池的大小,在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU的利用水平。
给出下列定义:
Ncpu = number of CPUs
Ucpu = target CPU utilization, 0 <= Ucpu <= 1
W/C = ratio of wait time to compute time
要使处理器到达期望的使用率,线程池的最优大小等于:
Nthreads = Ncpu * Ucpu * (1 + W/C)

可以通过Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntime().availableProcessors();

CPU周期并不是唯一影响线程池大小的资源,还包括内存,文件句柄,套接字句柄和数据库连接等。

8.3 配置ThreadPoolExecutor

ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由Executors中的newCachedThreadPool,newFixedThreadPool和newScheduledThreadPool等工厂方法返回的。ThreadPoolExecutor是一个灵活的,稳定的线程池,允许进行各种定制。

如果默认的执行策略不能满足需求,那么可以通过ThreadPoolExecutor的构造函数来实例化一个对象,

//        8-2 ThreadPoolExecutor的通用构造函数
public ThreadPoolExecutor(int corePoolSize,
   int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) { ... }

一个线程从被提交(submit)到执行共经历以下流程:

  1. 提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到线程数,则创建核心线程处理任务;否则,就执行下一步;
  2. 接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执行下一步;
  3. 接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出RejectedExecutionException异常。
  • 如果池中的实际线程数小于corePoolSize,无论是否其中有空闲的线程,都会给新的任务产生新的线程。
  • 如果池中的线程数>corePoolSize and <maximumPoolSize,而又有空闲线程,就给新任务使用空闲线程,如没有空闲线程,则产生新线程。
  • 如果池中的线程数=maximumPoolSize,则有空闲线程使用空闲线程,否则新任务放入workQueue。(线程的空闲只有在workQueue中不再有任务时才成立)

线程池提交流程

线程池在执行excute方法时,主要有以下四种情况:
1 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁)
2 如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue
3 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁)
4 如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
线程池创建流程

newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。
newCachedThreadPool工厂方法将线程池的最大大小设置为Integ.MAX_VALUE,而且将基本大小设置为0,并将超时设置为1分钟,这种方法创建的线程池可以被无限扩展,并且当需求降低时会自动收缩。

8.3.2 管理队列任务

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。
基本的任务排队方法有3种:无界队列(unbounded queue,),有界队列(bounded queue,)和同步移交(synchronous handoff)。
队列的选择与其他的配置参数有关,例如线程池的大小等。

newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续地达到,并且超过了线程池处理它们的速度,那么队列将无限制地增加。

一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。
有界队列有助于避免资源耗尽的情况发生,但队列填满后,新的任务怎么办(饱和策略了解一下)
在使用有界队列的时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,有助于减少内存使用量,减低CPU的使用率,同时还可以减少上下文切换,但代价是可能限制吞吐量。

对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。
SynchronousQueue不是一个真正的队列,而是一种在线程之间移交的机制。
要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么TrheadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。
使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是首先放在队列中,然后有工作者线程从队列中提取该任务。
只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。
在newCachedThreadPool工厂方法中使用了SynchronousQueue。

当使用像LinkedBlockingQueue或ArrayBlockingQueue这样的FIFO队列时,任务的执行顺序与到达顺序相同。
如果像进一步控制任务执行顺序,可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。任务的优先级时通过自然顺序或Comparator(如果任务实现了Comparable)来定义的。

只有任务相互独立时,为线程池或工作队列设置界限才是合理的。
如果任务之间存在依赖关系,那么有界的线程池或队列可能导致线程饥饿死锁问题。此时应该使用无界的线程池如newCachedThreadPool。

8.3.3 饱和策略

ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略)

  • AbortPolicy,“中止(Abort)策略”是默认的饱和策略,该策略将抛出未检查的Rejected-ExecutionException。调用这可以捕获这个异常,然后根据需求编写自己的处理代码。
  • DiscardOldestPolicy,“抛弃(Discard)策略”在当新提交的任务无法保存到队列中等待执行时生效。抛弃策略会抛弃下个将被执行任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么抛弃最旧策略将导致抛弃优先级最高的任务,因此最好不要将抛弃最旧饱和策略和优先级队列一起使用)
  • CallerRunsPolicy,“调用者运行(Caller-Runs)策略”,实现了一种调节机制。该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而减低新任务的流量。 它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。

我们以第6章的WebServer作为实例(如6.2.1),修改为使用有界队列和CallerRunsPolicy饱和策略。 当线程池中的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行。由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理正在执行的任务。在这期间,主线程不会调用accpet,因此到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中。如果持续过载,TCP层将发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终到达客户端,导致服务器在高负载下实现一种平缓的性能降低

当创建Executor时,可以选择饱和策略或者对执行策略进行修改。

//     8-3        创建一个固定大小的线程池,并采用有界队列以及“调用者运行”饱和策略
ThreadPoolExecutor executor
    = new ThreadPoolExecutor(N_THREADS, N_THREADS,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(CAPACITY));
     executor.setRejectedExecutionHandler(
     new ThreadPoolExecutor.CallerRunsPolicy());

当工作队列被填满后,没有预定义的饱和策略来阻塞execute。然而,通过使用Semaphore(信号量)来限制任务的到达率,就可以实现这个功能。
BoundedExecutor使用了一个无界队列(因为不能限制队列的大小和任务额达到率),并设置信号量的上界设置为线程池的大小加上可排队任务的数量,这是因为信号量需要控制正在执行的和等待执行的任务数量。

//          8-4     使用Semaphore来控制任务的提交速率
public class BoundedExecutor {
     private final Executor exec;
     private final Semaphore semaphore;

     public BoundedExecutor(Executor exec,int bound){
         this.exec=exec;
         this.semaphore=new Semaphore(bound); //创建一个信号量,大小为bound,信号量的计数值会初始化为容器容量的最大值
     }

     public void submitTask(final Runnable command) throws InterruptedException{
         semaphore.acquire();   //获得该信号量的许可, blocking 阻塞直到在得到有空闲的 或者线程被中断
         try{
             exec.execute(new Runnable(){             //工作者线程执行任务command
                 public void run(){
                     try{
                        command.run(); 
                     }finally{
                         semaphore.release();
                     }
                 }
             });
         }catch (RejectedExecutionException e) {
            semaphore.release();         //释放一个许可,使更多元素能添加到容器中
        }
     }
}
8.3.4 线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。

//      8-5  ThreadFactory接口   
public interface ThreadFactory {
    Thread newThread(Runnable r);
}

默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。
通过指定一个线程工厂方法,可以定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程都会调用这个方法。

但是很多情况下需要订制的线程工厂方法。下面就是例子

// 8-6 自定义的线程工厂
public class MyThreadFactory implements ThreadFactory {
    private final String poolName;  //线程池名字,方便识别

    public MyThreadFactory(String poolName) {
        this.poolName = poolName;
    }

    public Thread newThread(Runnable runnable) {
        return new MyAppThread(runnable, poolName);
    }
}

在MyAppThread中还可以定制其他行为,包括:为线程指定名字,设置自定义UncaughtException-Handler向Logger中写入信息,维护一些统计信息(包括有多少个线程被创建和销毁),以及在线程被创建或终止时把调试信息写入日志。

//          8-7   定制Thread基类
public class MyAppThread extends Thread{
      public static final String DEFAULT_NAME="MyAppThread";
      private static volatile boolean debugLifecycle=false;
      private static final AtomicInteger created=new AtomicInteger();   //记录创建线程的数量
      private static final AtomicInteger alive=new AtomicInteger();     //记录活跃的线程的数量
      private static final Logger log=Logger.getAnonymousLogger();  //创建日志

      public MyAppThread(Runnable r){
          this(r,DEFAULT_NAME);
      }

    public MyAppThread(Runnable runnable, String name) {
        //created.incrementAndGet()  以原子方式将当前值加 1,返回的最新值(即加1后的
        super(runnable,name+"-"+created.incrementAndGet()); //调用父类中的Thread(Runnable target, String name)方法,分配一个线程对象,名字加上了为第几个被创建线程
        setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {     //设置自定义UncaughtException-Handler向Logger中写入信息
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                //Logger.log(Level level, String msg, Throwable thrown),把信息写入日志
                log.log(Level.SEVERE, "UNCAUGHT in thread"+t.getName(),e);          
            }
        });

    }

    public void run(){
        //复制debug标志以确保一致的值
        boolean debug=debugLifecycle;
        if(debug) //如果debug为真                ,!debug如果debug为假
            log.log(Level.FINE,"Created"+getName());   //记录哪些线程被创建
        try{
            alive.incrementAndGet();
            super.run();
        }finally{
            alive.decrementAndGet();         //以原子方式将当前值减1。返回的最新值(即减1后的
            if(debug)
                //Logger.log(Level level, String msg),把信息写入日志
                log.log(Level.FINE, "Exiting"+getName());   //记录哪些线程被销毁
        }
    }

    public static int getThreadsCreated(){
        return created.get(); // 返回最新值 current,当前的
    }

    public static int getThreadsAlive(){
        return alive.get();
    }

    public static boolean getDebug(){
        return debugLifecycle;
    }

    public static void setDebug(boolean b){  //设置
        debugLifecycle=b;
    }
}

如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executor中的privilegedThreadFactory工厂方法(privileged,享有特权的)来定制自己的线程工厂。通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限(permissions),AccessControlContext和contextClassLoader。
如果不使用privilegedThreadFactory,线程池创建的线程将从在需要新线程调用execute或submit的客户程序中继承访问权限,从而导致令人困惑的安全性异常。

8.3.5 在调用构造函数后在定制ThreadPoolExecutor

在调用完ThreadPoolExecutor的构造函数后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数(例如线程池的基本大小,最大大小,存活时间,线程工厂以及拒绝执行处理器(rejected execution handler))。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。

//     8-8    对通过标准方法创建的Executor进行修改
ExecutorService exec = Executors.newCachedThreadPool();
   if (exec instanceof ThreadPoolExecutor)
       ((ThreadPoolExecutor) exec).setCorePoolSize(10);
   else
       throw new AssertionError("Oops, bad assumption");

在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回按这种方法包装的ExecutorService,而不是最初的ThreadPoolExecutor。虽然单线程的Executor实际上被实现为一个只包含唯一线程线程池,但它同样确保了不会并发地执行任务。如果在代码中增加单线程Executor的线程池大小,将破坏它的执行语义。

8.4 扩展ThreadPoolExecutor

ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute,afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。

在执行任务的线程中将调用beforeExecute和afterExecute等方法。
无论是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。(如果任务在完成后带有一个Error,那么就不会调用afterExecute)如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。

在线程池完成关闭时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。 terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知,记录日志或收集finalize统计信息等操作。

示例:给线程池添加统计信息

在TimingThreadPool中给出了一个自定义的线程池,它通过beforeExecute,afterExecute和terminated等方法来添加日志记录和统计信息收集。

//        8-9  增加了日志和计时等功能的线程池
public class TimingThreadPool  extends ThreadPoolExecutor{
	 //因为这些方法将在执行任务的线程中调用,因此beforeExecute可以把值保存到一个ThreadLocal变量中,然后由afterExecute来读取
     private final ThreadLocal<Long> startTime= new ThreadLocal<Long>();             
     private final Logger log=Logger.getLogger("TimingThreadPool");
     //记录有多少个任务执行完成了
     private final AtomicLong numTasks=new AtomicLong();    
      //记录总时间
     private final AtomicLong totalTime=new AtomicLong();  

     public TimingThreadPool() {
         super(1, 1, 0L, TimeUnit.SECONDS, null);
     }

     protected void beforeExecute(Thread t,Runnable r){
         super.beforeExecute(t, r);
         //记录fine级别的信息入日志,%s分别对应两个参数
         log.fine(String.format("Thread %s:start %s", t,r)); 
         //设置为当前系统时间纳米秒级别
         startTime.set(System.nanoTime());   
     }

     protected void afterExecute(Runnable r,Throwable t){
         try{
             long endTime=System.nanoTime();
             long taskTime=endTime-startTime.get();
             numTasks.incrementAndGet();   //原子操作的加
             totalTime.addAndGet(taskTime);   //原子操作的加上taskTime
             //记录fine级别的信息入日志,%s分别对应两个参数
             log.fine(String.format("Thread %s: end %s,time=%dns", t,r,taskTime));
         }finally{  
          //无论是正常返回还是抛出异常而返回,afterExecute都会被调用
             super.afterExecute(r, t);
         }  
     }
	 //输出平均时间
     protected void terminated(){  
         try{
         	//记录info级别的信息入日志,%对应参数
             log.info(String.format("Terminated: avg time=%dns", totalTime.get()/numTasks.get())); 
         }finally{
             super.terminated();
         }
     }
}

8.5 递归算法的并行化

在6.3节中,最后一次实现将每个图像的下载都视作一个独立任务,从而实现了更高的并行性。

如果在循环中包含了一些密集计算,或者需要执行可能阻塞的I/O操作,那么只要每次迭代是独立的,都可以对其进行并行化。

如果循环中的迭代操作都是独立的,并且不需要等待所有的迭代操作都完成在继续执行,那么就可以使用Executor将串行循环转化为并行循环,在processSequentially和processInParallel中给出了这种方法。(parallel 并行的 ,sequentially 继而地,顺序地)

//      8-10 将串行执行转换为并行执行(迭代操作)
void processSequentially(List<Element> elements) {  //串行
  for (Element e : elements)
  process(e);
}
void processInParallel(Executor exec, List<Element> elements) { //并行
  for (final Element e : elements)
  exec.execute(new Runnable() {
  public void run() { process(e); }
  });
}

当串行循环中的各个迭代操作之间彼此独立,并且每个迭代操作执行的工作量比管理一个新任务时带来的开销更多,那么这个串行循环就适合并行化。

在一些递归设计中同样可以采用循环并行化的方法。在递归算法中通常都会存在串行循环,而且这些循环可以按照程序8-10的方式并行化。

一种简单的情况是:在每个迭代操作中都不需要来自于后续递归迭代的结果。例如,8-11中的sequentialRecursive用深度优先算法遍历一棵树,在每个节点上执行计算并将结果放入一个集合。修改后的parallel同样执行深度优先遍历,但它并不是在访问节点时进行计算,而是为每个节点提交一个任务来完成计算。

//           8-11  将串行递归转换为并行递归
public<T> void sequentialRecursive(List<Node<T>> nodes,
                               Collection<T> results) {  //串行递归
     for (Node<T> n : nodes) {
         results.add(n.compute());
         sequentialRecursive(n.getChildren(), results);
     }
}
//       共享且可变的变量用final修饰
public<T> void parallelRecursive(final Executor exec,
                                 List<Node<T>> nodes,
                                 final Collection<T> results) { //并行递归
      for (final Node<T> n : nodes) {
          exec.execute(new Runnable() {
             public void run() {
                results.add(n.compute());
             }
          });
          parallelRecursive(exec, n.getChildren(), results);
      }
}

当parallelRecursive返回时,树中的各个节点都已经访问过了(但是遍历过程仍然是串行,只有compute是并行执行的),并且每个节点的计算也已经放入Executor的工作队列。

parallelRecursive的调用者可以通过以下方式等待所有的结果:创建一个特定于遍历过程的Executor,并使用shutdown和awaitTermination等方法,如下:

//     8-12   等待通过并行方式计算的结果
public<T> Collection<T> getParallelResults(List<Node<T>> nodes)
                                 throws InterruptedException {
    ExecutorService exec = Executors.newCachedThreadPool();
    Queue<T> resultQueue = new ConcurrentLinkedQueue<T>();
    parallelRecursive(exec, nodes, resultQueue);
    exec.shutdown();
    exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    return resultQueue;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值