并发实用工具

JDK8在线Api中文手册

JDK8在线Api英文手册

1 概述

   从一开始,Java就对多线程和同步提供了内置支持。例如,可以通过实现Runnable接口或扩展Thread类来创建新的线程;可以通过使用synchornized关键字来获得同步支持;并且Object类定义的wait和notify方法支持线程间通信。总之,这种对多线程的内置支持是Java最重要的革新之一,并且仍然是Java的主要优势之一。
   但是,因为Java对多线程的原始支持在概念上是简单的,并不是对所有应用来说都是理想选择,特别是那些大量使用多线程的情况。例如,原始的多线程支持并没有提供一些高级特性,比如信号量、线程池以及执行管理,这些特性有助于创建强大的并发程序。
   在一开始就需要重点解释的是:许多Java程序因为使用多线程,所以是"并发的"。例如,许多applet和servlet都使用多线程。但是,本篇使用的术语"并发程序"是指广泛而完整地执行多个并发线程的程序。这种程序的一个示例是使用不同的线程同步计算更大型计算的部分结果。另外一个示例是协调多个线程的活动,每个线程都试图访问数据库中的信息。在这种情况下,对只读访问的处理方式可能与那些需要读/写功能访问的处理方式不同。
   为了满足处理并发程序的需要,JDK5增加了并发实用工具,通常也被称为并发API。最初的并发实用工具集提供了开发并发应用程序的许多特性。例如,提供了同步器比如信号量、线程池、执行管理器、锁、一些并发集合以及使用线程获取计算结果的流线化方式。
   尽管原始的并发API也给人们留下了深刻的印象,但是JDK7对之进行了极大扩展。最重要的新增内容是Fork/Join框架。Fork/Join框架使得创建使用多个处理器(例如多核系统中的处理器)的程序更加容易,因而简化了两个或多个片真正同时执行(即真正的并行执行),而不仅仅是时间切片这类程序的创建。可以很容易想象到,并行执行可以显著提高特定操作的速度。因为多核系统正在普及,所以提供Fork/Join框架是很及时的,其功能也跟强大。随着JDK8的发布,Fork/Join框架也得到进一步增强。
   JDK8还引入了与并发API的其他部分相关的一些新特性。因此,并发API仍然在演化和扩展,以满足现代计算环境的需求。
   原始的并发API相当大,JDK7和JDK8的新增特性更是显著增加了这一API的大小。即使在没有严重依赖并行处理的程序中,同步器。可调用线程以及执行器这类特性,依然可以广泛应用于各种情况。可能最重要的是,因为多核系统的不断普及,涉及Fork/Join框架的解决方案变得更加普遍。由于这些原因,本篇简要介绍并发实用工具定义的一些核心功能,并显示使用它们的一些例子。最后以深度分析Fork/Join框架结束本篇。

2 并发 API包

   并发实用工具位于java.util.concurrent包及其两个子包中,这两个子包是java.util.concurrent.atomic和java.util.concurrent.locks。在此简要介绍它们的内容。

2.1 java.util.concurrent 包

   java.util.concurrent包定义了一些核心特征,用于以其他方式实现同步和线程间通信,而非使用内置方式。定义的关键特征有:

  • 同步器
  • 执行器
  • 并发集合
  • Fork/Join框架
       同步器提供了同步多线程交互的高级方法。java.util.concurrent包定义的同步器类如表1所示。
表1 java.util.concurrent 包定义的同步器类
描 述
Semaphore 实现经典的信号量
CountDownLatch 进行等待,直到发生指定数量的时间为止
CyclicBarrier 使一组线程在预定义的执行点进行等待
Exchanger 在两个线程之间交换数据
Phaser 对向前通过多阶段的线程进行同步

   注意每个同步器都为特定类型的同步问题提供了一种解决方案,这使得每个同步器对于各自的预期应用都进行过优化。在过去,这些类型的同步对象必须手工创建。并发API对于它们进行了标准化,使得它们能够被所有Java程序员使用。
   执行器管理线程的执行。在执行器层次的顶部是Executor接口,该接口用于启动线程。ExecutorService扩展了Executor,并提供了管理执行的方法。ExecutorService有3个实现:ThreadPoolExecutor、ScheduledThreadPoolExecutor和ForkJoinPool。java.util.concurrent包还定义了Executors使用工具类,该类包含大量的静态方法,可以简化各种执行器的创建。
   与执行器相关的是Future和Callable接口。Future包含一个值,该值是由线程在执行后返回的。因此,这个值是"在将来"——线程终止时定义的。Callable定义返回值的线程。
   java.util.concurrent包定义了几个并发集合类,包括ConcurrentHashMap、ConcurrentLinkedQueue和CopyOnWriteArrayList。这些类提供了由集合框架定义的相关类的并发替代版本。
   Fork/Join框架支持并行编程,包含的主要类有ForkJoinTask、ForkJoinPool、RecursiveTask以及RecursiveAction。
   最后,为了更好地处理线程计时,java.util.concurrent包定义了TimeUnit枚举。

2.2 java.util.concurrent.atomic 包

   java.util.concurrent.atomic包简化了并发环境中变量的使用,提供了一种高效更新变量值的方法,而不需要使用锁。这是通过使用一些类和方法完成的,例如AtomicInteger和AtomicLong类,以及compareAndSet()、decrementAndGet()和getAndSet()方法。这些方法执行起来就像单一的、非中断的操作那样。

2.3 java.util.concurrent.locks 包

   java.util.concurrent.locks包为同步方法的使用提供了一种替换方案。这种替代方案的核心是Lock接口,该接口定义了访问对象和放弃访问对象的基本机制。关键方法是lock()、tryLock()和unlock()。使用这些方法的优势是可以对同步进行进一步的控制。

3 使用同步对象

   同步对象由Semaphore、CountDownLatch、CyclicBarrier、Exchanger以及Phaser类支持。总的来说,通过它们可以比较容易地处理一些以前比较困难的同步情况。它们也可是被应用于广泛的应用程序中——甚至是那些只包含有限并发的程序。因为几乎所有Java程序都对同步对象感兴趣。

3.1 Semaphore 类

   Semaphore同步对象,实现了经典的信号量。信号量通过计数器控制对共享资源的访问。如果计数器大于0,访问是允许的;如果为0,访问是拒绝的。计数器允许访问共享资源的许可证。因此,为了访问资源,线程必须保证获取信号量的许可证。
通常,为了使用信号量,希望访问共享资源的线程尝试取得许可证。如果信号量的计数大于0,就表明线程取得许可证,这会导致信号量的计数减小;否则,线程会被阻塞,知道能够获取许可为止。当线程不再需要访问共享资源时,释放许可证,从而增大信号量的计数。如果还有另外一个线程正在等待许可证,该线程将在这一刻取得许可证。Java的Semaphore类实现了这种机制。
   Semaphore类具有如下所示的两个构造函数:

Semaphore(int num)
Semaphore(int num,boolean how)

   其中,num指定了初始的许可证计数大小。因此,num指定了任意时刻能够访问共享资源的线程数量。如果num是1,那么在任意时刻只有一个线程能够访问资源。默认情况下,等待线程以未定义的顺序获取许可证。通过将how设置为true,可以确保等待线程以它们要求访问的顺序获得许可证。
   为了得到许可证,可以调用acquire()方法,该方法具有以下两种形式:

void acquire() throws InterruptedException
void acquire(int num) throws InterruptedException

   第1种形式获得一个许可证。第2种形式获得num个许可证。在绝大多数情况下,使用第1种形式。如果在调用时无法取得许可证,就挂起调用线程,直到许可证可以获得为止。
   为了释放许可证,可以调用release()方法,该方法具有以下两种形式:

void release()
void release(int num)

   第1种形式释放一个许可证。第2种形式释放的许可证数量由num指定。
   为了使用信号量控制对资源的访问,在访问资源之前,希望使用资源的每个线程必须首先调用acquire()方法。当线程使用完资源时,必须调用release()方法。下面的例子演示了信号量的使用:

//A shared resource.
class Shared {
    static int count = 0;
}
//A thread of execution that increments count.
import java.util.concurrent.Semaphore;
class IncThread implements Runnable {
    String name;
    Semaphore sem;

    public IncThread(Semaphore s, String n) {
        sem = s;
        name = n;

        new Thread(this).start();
    }

    @Override
    public void run() {
        System.out.println("Starting " + name);
        try {
            //First,get a permit.
            System.out.println(name + " is waiting for a permit.");
            sem.acquire();
            System.out.println(name + " gets a permit.");
            //Now,access shared resource.
            for (int i = 0; i < 5; i++) {
                Shared.count++;
                System.out.println(name + ": " + Shared.count);
                //Now,allow a context switch -- if possible.
                Thread.sleep(10);
            }
        } catch (InterruptedException exc) {
            System.out.println(exc);
        }
//Release the permit.
        System.out.println(name + " release the permit.");
        sem.release();
    }
}
//A thread of execution that decrements count.
import java.util.concurrent.Semaphore;
class DecThread implements Runnable {
    String name;
    Semaphore sem;

    DecThread(Semaphore s, String n) {
        sem = s;
        name = n;
        new Thread(this).start();
    }

    @Override
    public void run() {
        System.out.println("Starting " + name);
        try {
            //First,get a permit.
            System.out.println(name + " is waiting for a permit.");
            sem.acquire();
            System.out.println(name + " gets a permit.");
            //Now,access shared resource.
            for (int i = 0; i < 5; i++) {
                Shared.count--;
                System.out.println(name + ": " + Shared.count);
                //Now,allow a context switch -- if possible.
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//Release the permit.
        System.out.println(name + " releases the permit.");
        sem.release();
    }
}
//A simple semaphore example.
import java.util.concurrent.Semaphore;
class SemDemo {
    public static void main(String[] args) {
        Semaphore sem = new Semaphore(1);
        new IncThread(sem, "A");
        new DecThread(sem, "B");
    }
    /**
     * 输出
     * Starting A
     * A is waiting for a permit.
     * A gets a permit.
     * Starting B
     * B is waiting for a permit.
     * A: 1
     * A: 2
     * A: 3
     * A: 4
     * A: 5
     * A release the permit.
     * B gets a permit.
     * B: 4
     * B: 3
     * B: 2
     * B: 1
     * B: 0
     * B releases the permit.
     */
}

   该程序使用信号量控制对count变量的访问,count变量是Shared类中的静态变量。IncThread()类的run()方法将Shared.count增加5次,DecThread类则将Shared.count减小5次。为了防止这两个线程同时访问Shared.count,只有在从控制信号量取得许可之后才允许访问。完成访问之后,释放许可证。通过这种方式,每次只允许一个线程访问Shared.count,正如输出所显示的。
   注意,IncThread和DecThread都在run()方法中调用了sleep()方法。这样做的目的是"证明"对Shared.count的访问是通过信号量进行同步的。在run()方法中调用sleep()方法,会导致调用线程在两次访问Shared.count之间暂停一段时间。正常情况下,这会使第二个线程得以运行。但是,因为信号量,第二个线程必须等待,直到第一个线程释放许可证。因此,Shared.count首先被IncThread增加5次,然后被DecThread减小5次。增加和减小操作不会交叉进行。
   如果没有使用信号量,那么两个线程对Shared.count的访问可能会同时发生,并且增加和减小操作会同时发生。为了证实这种情况,尝试注释掉对acquire()和release()方法的调用。当运行该程序时,会发现对Shared.count的访问不再是同步的,并且每个线程一旦得到时间片,就立即访问。
   尽管对信号量的使用在许多情况下很直观,正如前面的程序所显示的,但是更令人迷惑的使用也是可能的。下面是一个例子,使用两个信号量管理生产者和使用者线程,确保对put()方法的每次调用都跟有相应的get()调用:

//An implementations of a producer and consumer
//that use semaphoers to control synchronization.
import java.util.concurrent.Semaphore;
class Q {
    int n;
    //Start with consumer semaphoer unavailable.
    static Semaphore semCon = new Semaphore(0);
    static Semaphore semPord = new Semaphore(1);

    void get() {
        try {
            semCon.acquire();
        } catch (InterruptedException e) {
            System.out.println("InterruptedException caught");
        }
        System.out.println("Got: " + n);
        semPord.release();
    }

    void put(int n) {
        try {
            semPord.acquire();
        } catch (InterruptedException e) {
            System.out.println("InterruptedException caught");
        }
        this.n = n;
        System.out.println("Put: " + n);
        semCon.release();
    }
}
class Producer implements Runnable {
    Q q;

    public Producer(Q q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            q.put(i);
        }
    }
}
class Consumer implements Runnable {
    Q q;

    public Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            q.get();
        }
    }
}
class ProdCon {
    public static void main(String[] args) {
        Q q = new Q();
        new Consumer(q);
        new Producer(q);
        /**
         * 输出
         * Put: 0
         * Got: 0
         * Put: 1
         * Got: 1
         * Put: 2
         * Got: 2
         * Put: 3
         * Got: 3
         * Put: 4
         * Got: 4
         * Put: 5
         * Got: 5
         * Put: 6
         * Got: 6
         * Put: 7
         * Got: 7
         * Put: 8
         * Got: 8
         * Put: 9
         * Got: 9
         * Put: 10
         * Got: 10
         * Put: 11
         * Got: 11
         * Put: 12
         * Got: 12
         * Put: 13
         * Got: 13
         * Put: 14
         * Got: 14
         * Put: 15
         * Got: 15
         * Put: 16
         * Got: 16
         * Put: 17
         * Got: 17
         * Put: 18
         * Got: 18
         * Put: 19
         * Got: 19
         */
    }
}

   可以看出,对put()和get()方法的调用是同步的。也就是说,对put()方法的每次调用都会跟随相应的get()调用,没有丢失数值。如果不使用信号量,那么put()调用可能发生多次,并且没有发生匹配的get()调用,结果会丢失数值(为了证明这一点,可以删除信号量代码进行观察)。
   put()和get()方法的调用顺序是通过两个信号量处理的:semProd和semCon。put()方法在能够产生数值之前,必须从semProd获得许可证。在设置数值之后,释放semCon。get()方法在能够使用数量之前,必须从semCon获得许可证。使用完数值之后,释放semProd。这种"给予与获取"机制确保对put()方法的每次调用,后面都跟随相应的get()调用。
   注意,semCon最初被初始化为没有可用许可证。这样可以保证首先执行put()。设置初始同步状态的能力是信号量功能更为强大的一个方面。

3.2 CountDownLatch 类

   有时会希望线程进行等待,直到发生一个或多个事件为止。为了处理这类情况,并发API提供了CountDownLatch类。CountDownLatch在初始创建时带有事件数量计数器,在释放锁存器之前,必须发生指定数量的事件。每次发生一个事件时,计数器递减。当计数器达到0时,打开锁存器。
   CountDownLatch具有以下构造函数:

CountDownLatch(int num)

   其中,num指定了为打开锁存器而必须发生的时间数量。
   为了等待锁存器,线程需要调用await()方法,该方法的形式如下所示:

void await()throws InterruptedException
boolean await(long wait,TimeUnit tu)throws InterruptedException

   对于第1种形式,直到与调用CountDownLatch对象关联的计数器达到0才结束等待。第2种形式只等待由wait指定的特定时间。wait表示的单位由tu指定,tu是一个TimeUnit枚举对象。如果到达时间限制,将返回fasle;如果倒计时达到0,将返回true。
   为了触发事件,可以调用countDown()方法,该方法如下所示:

void countDown()

   对countDown()方法的每次调用,都会递减与调用对象关联的计数器。
   下面的程序演示了CountDownLatch。该程序创建一个锁存器,在打开这个锁存器之前,需要发生5个时间。

import java.util.concurrent.CountDownLatch;
class MyThread implements Runnable {
    CountDownLatch latch;
    public MyThread(CountDownLatch c) {
        latch = c;
        new Thread(this).start();
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            latch.countDown();//decrement count
        }
    }
}
//An example of CountDownLatch.
import java.util.concurrent.CountDownLatch;
class CDLDemo {
    public static void main(String[] args) {
        CountDownLatch cdl = new CountDownLatch(5);
        System.out.println("Starting");
        new MyThread(cdl);
        try {
            cdl.await();
        }catch (InterruptedException exc){
            System.out.println(exc);
        }
        System.out.println("Done");
    }
    /**
     * 输出:
     * Starting
     * 0
     * 1
     * 2
     * 3
     * 4
     * Done
     */
}

   在main()方法中,创建CountDownLatch对象cdl,并将之初始化为5。接下来,创建一个MyThread实例,该实例开始一个新线程的执行。注意cdl被作为参数传递给MyThread的构造函数,并保存在latch实例变量中。然后,主线程对cdl调用await()方法,这会暂停主线程的执行,直到cdl的计数器被递减5次为止。
   在MyThread的run()方法中,创建一个迭代5次的循环。每次迭代时,对latch调用countDown()方法,latch引用main()方法中的cdl。迭代5次后,打开锁存器,从而允许恢复主线程。
   CountDownLatch是功能强大、易于使用的同步对象。无论何时,只要线程必须等待一个或多个事件发生,这个对象就适用。

3.3 CyclicBarrier 类

   在并发编程中,下面这种情况时很常见的:具有两个或多个线程的线程组必须在预定执行点进行等待,直到线程组中的所有线程都到达执行点为止。为了处理这种情况,并发API提供了CyclicBarrier类。使用CyclicBarrier类可以定义具体以下特点的同步对象,同步对象会被挂起,直到指定数量的线程都达到界限点为止。
   CyclicBarrier类具有以下两个构造函数:

CyclicBarrier(int numThreads)
CyclicBarrier(int numThreads,Runnable action)

   其中,numThreads指定了在继续执行之前必须到达界限点的线程数量。在第2种形式中,action指定了当到达界限点时将要执行的线程。
   下面是使用CyclicBarrier时需要遵循的一般过程。首先,创建一个CyclicBarrier对象,指定将进行等待的线程数量。接下来,当每个线程都到达界限点时,对CyclicBarrier对象调用await()方法。这将暂停线程的执行,直到所有其他线程也调用await()方法为止。一旦指定数量的线程到达界限点,await()方法将返回并恢复执行。此外,如果指定某个操作,那么将会执行那个线程。
   await()方法具有以下两种形式:

int await() throws InterruptedException.BrokenBarrierException
int await(long wait,TimeUnit tu) throws InterruptedException,BrokenBarrierException,TimeoutException

   第1种形式进行等待,直到所有线程都到达界限点为止。第2种形式只等待由wait指定的时间,wait使用的单位由tu指定。这两种形式都返回一个值,用于指示线程到达界限点的顺序。第1个线程返回的值等于等待的线程数减1,最后一个线程返回0。
   下面的例子演示了CyclicBarrier。该例等待由3个线程组成的线程组到达界限点,当所有线程到达界限点时,执行由BarAction指定的线程。

//A thread of execution that uses a CyclicBarrier.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
class MyThread implements Runnable {
    CyclicBarrier cbar;
    String name;
    public MyThread(CyclicBarrier c, String n) {
        cbar = c;
        name = n;
        new Thread(this).start();
    }
    @Override
    public void run() {
        System.out.println(name);
        try {
            cbar.await();
        } catch (InterruptedException | BrokenBarrierException exc) {
            System.out.println(exc);
        }
    }
}
//An object of this class is called when the CyclicBarrier ends.
class BarAction implements Runnable{
    @Override
    public void run() {
        System.out.println("Barrier Reached!");
    }
}
//An example of CyclicBarrier.
import java.util.concurrent.CyclicBarrier;
class BarDemo {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(3, new BarAction());
        System.out.println("Starting");
        new MyThread(cb, "A");
        new MyThread(cb, "B");
        new MyThread(cb, "C");
        /**
         * 输出(线程执行的精确顺序可能与此不同):
         * Starting
         * A
         * B
         * C
         * Barrier Reached!
         */
    }
} 

   CyclicBarrier可以重用,因为每次在指定数量的线程调用await()方法后,就会释放等待线程。例如,如果修改前面程序中的main()方法,使其看起来如下所示:

//An example of CyclicBarrier.
import java.util.concurrent.CyclicBarrier;
class BarDemo {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(3, new BarAction());
        System.out.println("Starting");
        new MyThread(cb, "A");
        new MyThread(cb, "B");
        new MyThread(cb, "C");
        new MyThread(cb, "X");
        new MyThread(cb, "Y");
        new MyThread(cb, "Z");
        /**
         * 输出(线程执行的精确顺序可能与此不同):
         * Starting
         * A
         * B
         * C
         * Barrier Reached!
         * X
         * Y
         * Z
         * Barrier Reached!
         */
    }
}

   正如前面的例子所显示的,对于之前很复杂的问题,CyclicBarrier提供了流线话的解决方案。

3.4 Exchanger 类

   同步类Exchanger,其谁目的是简化两个线程之间的数据交换。Exchanger对象的操作:简单地进行等待,直到两个独立的线程调用exchange()方法为止。当发生这种情况时,交换线程提供的数据。Exchanger的用途很容易就能形象处理。例如,一个线程可能为通过网络接受信息准备好一个缓冲区,另一个线程可能使用来自连接的信息填充这个缓冲区。则两个线程可以协同工作,每当需要一个新的缓冲区时,就进行一次数据交换。
   Exchanger是泛型类,其声明如下所示:

Exchanger<V>

   其中,V指定了将要进行交换的数据的类型。
   Exchanger定义的唯一方法是exchange(),该方法具有如下所示的两种形式:

V exchange(V objRef) throws InterruptedException
V exchange(V objRef,long wait,TimeUnit tu) throws InterruptedException,TimeoutException

   其中,objRef是要交换的数据的引用,从另一个线程接收的数据被返回。第2种形式的exchange()方法允许指定超时时间。exchange()方法的关键在于,直到同一个Exchanger对象被两个独立的线程分别调用后,该方法才会成功返回。因此,exchange()方法可以同步数据的交换。
   下面是一个演示Exchanger的例子。该例创建两个线程。其中一个线程创建一个空缓冲区,用于接收数据,另一个线程将数据放入这个空的缓冲区。本例使用的数据是字符串。因此,第一个线程使用空字符串交换满字符串。

//A Thread that uses a string.
import java.util.concurrent.Exchanger;
class UseString implements Runnable {
    Exchanger<String> ex;
    String str;
    public UseString(Exchanger<String> ex) {
        this.ex = ex;
        new Thread(this).start();
    }
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                //Exchange an empty buffer for a full one.
                str = ex.exchange(new String());
                System.out.println("Got: "+str);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}
//A Thread that constructs a string.
import java.util.concurrent.Exchanger;
class MakeString implements Runnable {
    Exchanger<String> ex;
    String str;
    public MakeString(Exchanger<String> ex) {
        this.ex = ex;
        str = new String();
        new Thread(this).start();
    }
    @Override
    public void run() {
        char ch = 'A';
        for (int i = 0; i < 3; i++) {
            //Fill Buffer
            for (int j = 0; j < 5; j++) {
                str += ch++;
            }
            try {
                //Exchange a full buffer for an empty one.
                str = ex.exchange(str);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}
//An example of Exchanger.
import java.util.concurrent.Exchanger;
class ExgrDemo {
    public static void main(String[] args) {
        Exchanger<String> exgr = new Exchanger<>();
        new UseString(exgr);
        new MakeString(exgr);
        /**
         * 输出:
         * Got: ABCDE
         * Got: FGHIJ
         * Got: KLMNO
         */
    }
}

   在该程序中,main()方法为字符串创建一个Exchanger对象。然后使用这个对象同步MakeString和UseString之间的字符串交换。MakeString类使用数据填充字符串。UseString类使用字符串使用空字符串交换满字符串。然后显示新构造的字符串的内容。空缓冲区和满缓冲区之间的交换通过exchange()方法进行同步,该方法是由这两个类(MakeString和UseString)的run()方法调用的。

3.5 Phaser 类

   同步类Phaser,主要目的是允许表示一个或多个活动阶段的线程进行同步。例如,可能有一组线程实现了订单处理应用程序的3个阶段。第1个阶段,每个线程分别用于确认客户信息、检查清单和确认定价。当该阶段完成后,第2阶段有两个线程,用于计算运算成本以及适用的所有税收。之后,最后一个阶段确定支付并判定大致的运输时间。
   首先,Phaser类除了支持多个阶段之外,其工作方式与上述CyclicBarrier类似。因此,通过Phaser可以定义等待特定阶段完成的同步对象。然后推进到下以阶段,再次进行等待,直到那一阶段完成为止。Phaser的应为与CyclicBarrier非常相似。但是,Phaser的主要用途是同步多个阶段。
   Phaser定义了4个构造函数,下面是本篇要使用的两个:

Phaser()
Phaser(int numParties)

   第1个构造函数创建的Phaser对象,注册party的数量为0。第2个构造函数注册的party的数量设置为numParties。术语"party"经常被应用于使用Phaser注册的对象,相当于线程的意思。尽管注册party的数量和将被同步的线程的数量通常是一致的,但这不是必需的。对于这两个构造函数,当前阶段都是0。即创建Phaser对象时,最初位于阶段0。
   一般来说,使用Phaser的方式如下:首先,创建一个新的Phaser实例。接下来,为该Phaser实例注册一个或多个party,具体做法是调用register()方法或者在构造函数中指定party的数量。对于每个注册的party,Phaser对象会进行等待,指定注册的所有party完成该阶段为止。party通过调用Phaser定义的多个方法之一来通知这一情况,例如arrive()或arriveAndAdvance()方法。所有party都达到后,该阶段就完成了,并且Phaser对象可以推进到下一阶段或终止。
   构造好Phaser对象后,为了注册party,可以调用register()方法。该方法如下所示:

int register()

   该方法返回注册party的阶段编号。
   为了通知party已经完成某个阶段,必须调用arrive()或arriveAndAwaitAdvance()方法的一些变体。当到达的任务数量等于注册的party数量时,该阶段就完成了,并且Phaser推进到下一阶段(如果有的话)。arrive()方法的一般形式如下所示:

int arrive()

   这个方法用于通知party(通常是执行线程)已经完成了某些任务(或任务的一部分)。该方法返回当前阶段编号。如果Phaser对象已经终止,就返回一个负值。arrive()方法不会挂起调用线程的执行。这意味着不会等待该阶段完成。这个方法只能通过已经注册的party进行调用。
   如果希望指示某个阶段已经完成,然后进行等待,直到所有其他注册party也完成该阶段为止,那么需要使用arriveAndAwaitAdvance()方法。该方法如下所示:

int arriveAndAwaitAdvance()

   该方法会进行等待,直到所有party到达。该方法返回下一阶段的编号;或者如果Phaser对象已经终止,就返回一个负值。这个方法只能通过已经注册的party进行调用。
   通过调用arriveAndDeregister()方法,线程可能在到达时注销自身。该方法如下所示:

int arriveAndDeregister()

   该方法返回当前阶段编号:或者如果Phaser对象已经终止,就返回一个负值。它不等待该阶段完成。这个方法只能通过已经注册的party进行调用。
   为了获取当前阶段编号,可以调用getPhase()方法,该方法如下所示:

final int getPhase()

   当Phaser对象创建时,第1阶段的编号将是0,第2阶段是1,第3阶段是2等等。如果调用Phaser对象已经终止,就返回一个负值。
   下面的例子显示了Phaser的使用。该例创建了3个线程,每个线程拥有3个阶段。该例使用Phaser同步每个阶段。

//A thread of execution that uses a Phaser.
import java.util.concurrent.Phaser;
class MyThread implements Runnable {
    Phaser phsr;
    String name;
    public MyThread(Phaser phsr, String name) {
        this.phsr = phsr;
        this.name = name;
        phsr.register();
        new Thread(this).start();
    }
    @Override
    public void run() {
        System.out.println("Thread " + name + " Beginning Phase One");
        phsr.arriveAndAwaitAdvance();//Signal arrival.
        //Phase a bit a prevent jumbled output.This is for illustration
        //only.It is not required for the proper operation of the phaser.
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread " + name + " Beginning Phase Two");
        phsr.arriveAndAwaitAdvance();//Signal arrival.
        //Pause a bit to prevent jumbled output.This is for illustration
        //only.It is not required for the proper operation of the phaser.
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread " + name + " Beginning Phase Three");
        phsr.arriveAndDeregister();//Signal arrival and deregister.
    }
}
//An example of Phaser.
import java.util.concurrent.Phaser;
class PhaserDemo {
    public static void main(String[] args) {
        Phaser phsr = new Phaser(1);
        int curPhase;
        System.out.println("Starting");
        new MyThread(phsr, "A");
        new MyThread(phsr, "B");
        new MyThread(phsr, "C");
        //Wait for all threads to complete phase one.
        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " Complete");
        //Wait for all threads to complete phase two.
        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " Complete");
        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " Complete");
        //Deregister the main thread.
        phsr.arriveAndDeregister();
        if (phsr.isTerminated()) {
            System.out.println("The Phaser is terminated");
        }
        /**
         * 输出:
         * Starting
         * Thread A Beginning Phase One
         * Thread B Beginning Phase One
         * Thread C Beginning Phase One
         * Phase 0 Complete
         * Thread B Beginning Phase Two
         * Thread C Beginning Phase Two
         * Thread A Beginning Phase Two
         * Phase 1 Complete
         * Thread C Beginning Phase Three
         * Thread A Beginning Phase Three
         * Thread B Beginning Phase Three
         * Phase 2 Complete
         * The Phaser is terminated
         */
    }
}

   首先,在main()方法中创建Phaser对象phsr,初始party数量为1(初始party对应主线程)。然后通过创建3个MyThread对象来启动3个线程。注意,为MyThread对象传递指向phsr(Phaser对象)的引用。MyThread对象使用这个Phaser对象来同步它们的活动。接下来,在main()方法中调用getPhase()方法以获取当前阶段编号(最初为0),然后调用arriveAndAwaitAdvance()方法,这会导致main()挂起,直到阶段0完成为止。在所有MyThread对象调用arriveAndAwaitAdvance()方法之前,阶段0不会完成。当阶段0完成时,main()恢复执行,这时显示阶段0已经完成,并推进到阶段1。重复这个过程,直到所有3个阶段完成。然后,在main()方法中调用arriveAndDeregister()方法。这时,所有3个MyThread对象将被注销。因为这会造成当Phaser推进到下一个阶段时产生未注册的party,所以Phaser终止。
   现在分析下一个MyThread类。首先,注意为构造函数传递一个引用,这个引用指向将要使用的Phaser对象,然后将新线程作为party在那个Phaser对象上进行注册。因袭,每个新的MyThread对象会变成一个party,它们使用传递的Phaser对象进行注册。因此,每个新的MyThread对象将会变成一个party,它们使用传递的Phaser对象进行注册。还需注意,每个线程都有3个阶段。在这个例子中,每个阶段由一个占位符构成,占位符显示线程的名称以及正在执行的工作。在前两个阶段之间,线程调用arriveAndAwaitAdvance()方法。因此,每个线程都会等待,直到所有线程完成那一阶段(并且主线程已经准备好)为止。所有线程(包括主线程)都到达后,Phaser推进到下一阶段。第3个阶段完成后,每个线程通过调用arriverAndAwaitDeregister()方法注销自身。正如MyThread()中的注释所解释的,调用sleep()方法是为了进行演示,以确保输出不会因为多线程而变得杂乱。Phaser能够正确工作,这与它们无关。如果删除它们,输出可能会有点乱,但是个阶段仍然会正确地同步。
   另外一点:尽管前面程序使用的3个线程属于相同的类型,但这不是必需的。使用Phaser的每个party可以是唯一的,每个party执行一些独立的任务。
   当推进阶段时,可以精确控制发生的操作。为此,需要重写advance()方法。这个方法在Phaser从一个阶段推进到下一个阶段时由运行时调用,该方法的一般形式如下所示:

protected boolean onAdvance(int phase,int numParties)

   其中,phase将包含推进之前的当前阶段编号,numParties将包含已注册party的数量。为了终止Phaser,onAdvance()方法必须返回true。为了保持Phaser活跃,onAdvance()方法必须返回false。当没有注册的party时,onAdvence()方法的默认版本返回true(因此会终止Phaser)。作为通用规则,对这一方法的重写应当遵循上述规则。
   重写onAdvance()方法的其中一个原因是:使Phaser执行指定数量的阶段,然后结束。下面的例子演示了这种用法。该例创建MyPhaser类,该类扩展了Phaser。将运行指定数量的阶段,这是通过重写onAdvance()方法来完成的。MyPhaser类的构造函数接受一个参数,这个参数指定要执行的阶段数量。注意,MyPhaser自动注册了一个party。在这个例子中,这种行为是有用的,但是不同应用程序的需求可能是不同的。

//Extend MyPhaser to allow only a specific number of phases
//to be executed.
import java.util.concurrent.Phaser;
class MyPhaser extends Phaser {
    int numPhases;
    MyPhaser(int parties,int phaseCount){
        super(parties);
        numPhases=phaseCount-1;
    }
    //Override onAdvance() to execute the specified
    //number of phases.
    @Override
    protected boolean onAdvance(int phase, int registeredParties) {
        //This println() statement is for illustration only.
        //Normally,onAdvance() will not display output.
        System.out.println("Phase "+phase+" completed.");
        //If all phases have completed,return true
        if(phase==numPhases||registeredParties==0){
            return true;
        }
        //Otherwise,return false.
        return false;
    }
}
//A thread of execution that uses a Phaser.
import java.util.concurrent.Phaser;
class MyThread implements Runnable {
    Phaser phsr;
    String name;
    public MyThread(Phaser phsr, String name) {
        this.phsr = phsr;
        this.name = name;
        phsr.register();
        new Thread(this).start();
    }
    @Override
    public void run() {
       while (!phsr.isTerminated()){
           System.out.println("Thread "+name+" Beginning Phase "+phsr.getPhase());
           phsr.arriveAndAwaitAdvance();
           //Pause a bit to prevent jumbled output.This is for illustration
           //only.It is required for the proper operation of the phaser.
           try {
               Thread.sleep(10);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
}
class PhaserDemo2 {
    public static void main(String[] args) {
        MyPhaser phsr = new MyPhaser(1,4);
        System.out.println("Starting");
        new MyThread(phsr,"A");
        new MyThread(phsr,"B");
        new MyThread(phsr,"C");
        //Wait for the specified number of phases to complete.
        while(!phsr.isTerminated()){
            phsr.arriveAndAwaitAdvance();
        }
        System.out.println("The Phaser is terminated");
        /**
         * 输出:
         * Starting
         * Thread A Beginning Phase 0
         * Thread B Beginning Phase 0
         * Thread C Beginning Phase 0
         * Phase 0 completed.
         * Thread C Beginning Phase 1
         * Thread B Beginning Phase 1
         * Thread A Beginning Phase 1
         * Phase 1 completed.
         * Thread B Beginning Phase 2
         * Thread C Beginning Phase 2
         * Thread A Beginning Phase 2
         * Phase 2 completed.
         * Thread C Beginning Phase 3
         * Thread B Beginning Phase 3
         * Thread A Beginning Phase 3
         * Phase 3 completed.
         * The Phaser is terminated
         */
    }
}

   在main()方法中,创建一个Phaser实例。传递4作为参数,这意味着将执行4个阶段,然后停止。接下来,创建3个线程,然后进入下面的循环:

//Wait for the specified number of phases to complete.
while(!phsr.isTerminated){
   phsr.arriveAndAwaitAdvance();
}

   这个循环简单地调用arriveAndAwaitAdvance()方法,直到Phaser终止为止。只有在执行完指定的阶段数之后,Phaser才会终止。在这个例子中,循环会继续执行,直到运行完4个阶段为止。接下来,注意线程也在循环中调用arriveAndAwaitAdvance()方法,循环直到Phaser终止时才会结束运行。这意味着它们会一直执行,直到已经执行完指定数量的阶段为止。
   现在,详细分析onAdvance()方法的代码。每次调用onAdvance()方法时,都传递当前阶段和注册的party数量。如果当前阶段等于指定阶段,或者如果注册的party数量是0,onAdvance()方法将返回true,因此停止Phaser。这是通过下面的代码来完成的:

//If all phases have completed,return true
if(phase==numPhases||registeredParties==0)
{
       return true;
 }

   如果只是为了重写onAdvance()方法,那么不必显示地扩展Phaser,就像前面的例子那样。在有些情况下,可以通过使用匿名内部类重写onAdvance()方法,从而创建更紧凑地代码。
   Phaser还有一个附加功能,可以通过调用awaitAdvance()方法等待待定的阶段,该方法如下所示:

int awaitAdvance(int phase)

   其中,phase指定阶段编号,awaitAdvance()方法将在指定阶段进行等待,直到下一个阶段发生为止。如果传递给phase的参数不等于当前阶段,将立即返回;如果Phaser已经终止,也将立即返回。但是,如果phase传递的是当前阶段,那么将会等待,直到阶段推进。注册的party只能调用该方法一次。这个方法还有一个可中断版本:awaitAdvanceInterruptibly()。
   为了注册多个party,可以调用bulkRegister()方法。为了获取已注册party的数量,可以调用getRegisterParties()方法。还可以通过getArrivedParties()和getUnarrivedParties(),分别获取已经到达的party数量和未到达的party数量。为了强制Phaser进入终止状态,可以调用forceTermination()方法。
   Phaser类也允许创建Phaser对象树。这是通过两个附加构造函数和getParent()方法得以实现的,这两个构造函数允许指定父Phaser对象。

4 使用执行器

   并发API提供了一种称为执行器的特性,用于启动并控制线程的执行。因此,执行器为通过Thread类管理线程提供了一种替代方案。
   执行器的核心是Executor接口,该接口定义了以下方法:

void execute(Runnable thread)

   由thread指定的线程将被执行。因此,execute()方法可以启动指定的线程。
   ExecutorService接口通过添加用于帮助管理和控制线程执行的方法,对Executor接口进行了扩展。例如,ExecutorService定义了shutdown()方法,该方法如下所示,用于停止调用ExecutorService对象:

void shutdown()

   ExecutorService还定义了一些方法,用来执行能够返回结果的线程、执行一组线程以及确定关闭状态。
   执行器还定义了ScheduledExecutorService接口,该接口扩展了ExecutorService以支持线程调度。
   并发API预定义了3个执行器类:ThreadPoolExecutor、ScheduledThreadExecutor以及ForkJoinPool。ThreadPoolExecutor实现了Executor和ExecutorService接口,并且对受管理的线程池提供支持。ScheduledThreadPoolExecutor还实现了ScheduledExecutorService接口,以允许调度线程池。ForkJoinPool实现了Executor和ExecutorService接口,用于Fork/Join框架。
   线程池提供了用于执行各种任务的一组线程。每个任务不是使用自己的线程,而是使用线程池中的线程,从而减轻了创建许多独立线程带来的负担。尽管可以直接使用ThreadPoolExecutor和ScheduledThreadPoolExecutor,但是在绝大多数情况下,会希望通过调用Executors实用工具类定义的以下静态工厂方法来获取执行器:

static ExecutorService newCachedThreadPool()
static ExecutorSevice newFixedThreadPool(int numThreads)
static ScheduledExecutorService newScheduledThreadPool(int numThreads)

   newCachedThreadPool()方法创建的线程池可以根据需要添加线程,但是会尽可能地重用线程。newFixedThreadPool()方法创建包含指定数量线程的线程池。newScheduledThreadPool()方法创建支持线程调度的线程池。每个方法都返回指定ExecutorService对象的引用,ExecutorService对象可以用于管理线程池。

4.1 一个简单的执行器示例

   下面的程序创建了一个固定线程池,该线程池包含两个线程。然后使用线程池执行4个任务。因此,4个任务共享线程池中的两个线程。完成任务后,关闭线程池,程序结束。

import java.util.concurrent.CountDownLatch;
class MyThread implements Runnable {
    String name;
    CountDownLatch latch;
    public MyThread(CountDownLatch latch,String name) {
        this.name = name;
        this.latch = latch;
        new Thread(this);
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + ":"+i);
            latch.countDown();
        }
    }
}
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//A simple example that uses an Executor.
class SimpExec {
    public static void main(String[] args) {
        CountDownLatch cdl = new CountDownLatch(5);
        CountDownLatch cdl2 = new CountDownLatch(5);
        CountDownLatch cdl3 = new CountDownLatch(5);
        CountDownLatch cdl4 = new CountDownLatch(5);
        ExecutorService es = Executors.newFixedThreadPool(2);
        System.out.println("Starting");
        //Start the threads.
        es.execute(new MyThread(cdl, "A"));
        es.execute(new MyThread(cdl2, "B"));
        es.execute(new MyThread(cdl3, "C"));
        es.execute(new MyThread(cdl4, "D"));
        try {
            cdl.await();
            cdl2.await();
            cdl3.await();
            cdl4.await();
        } catch (InterruptedException exc) {
            System.out.println(exc);
        }
        es.shutdown();
        System.out.println("Done");
        /**
         * 输出(线程执行的精确顺序可能与此不同):
         * Starting
         * A:0
         * A:1
         * A:2
         * A:3
         * A:4
         * C:0
         * C:1
         * C:2
         * C:3
         * C:4
         * D:0
         * D:1
         * D:2
         * D:3
         * D:4
         * B:0
         * B:1
         * B:2
         * B:3
         * B:4
         * Done
         */
    }
}

   正如输出所显示的,尽管线程池只包含两个线程,但所有4个任务仍会全部执行。不过,在同一时刻只能运行两个任务。其他任务必须等待。直到线程池中的某个线程可以使用为止。
   shutdown()方法调用很重要。如果在该程序中没有使用这个方法调用,程序终止,因为执行器仍然是活动的。

4.2 使用Callable 和 Future接口

   并发API最有趣的特性之一是Callable接口,这个接口表示返回值的线程。应用程序可以使用Callable对象计算结果,将结果返回给调用线程。这是一种功能强大的机制,因为可以简化对许多类型数值进行计算(其中部分结果需要同步计算)的编码工作,另外还可以用于运行那些返回状态码(用于指示线程成功完成)的线程。
   Callable是泛型接口,其定义如下所示:

interface Callable<V>

   其中,V指明了由任务返回的数据的类型。Callable只定义了一个方法,名为call,该方法如下所示:

V call() throws Exception

   在call()方法中定义希望执行的任务,在任务完成后返回结果。如果不能计算结果,那么call()方法必须抛出异常。
   Callable任务通过对ExecutorService对象调用submit()方法来执行。submit()方法有3种形式,但是只有一种用于执行Callable任务,如下所示:

<T> Future<T> submit(Callable<T> task)

   其中,task是将自己的线程中执行的Callabe对象,结果通过Future类型的对象得以返回。
   Future是泛型接口,表示将由Callable对象返回的值。因为这个值是建立的某个时间获取的,所以使用Future作为名称是合适的。Future的定义如下所示:

interface Future<V>

   其中,V指定了结果的类型。
   为了获得返回值,需要调用Future的get()方法,该方法具有以下两种形式:

V get() throws InterruptedException,ExecutionException
V get(long wait,TimeUnit tu) throws InterruptedException,ExecutionException,TimeoutException

   第1种形式无限期地等待结果。第2种形式允许使用wait指定超时时间。wait的单位是通过tu传递的,也就是TimeUnit枚举对象。
   下面的程序通过创建3个执行不同计算的任务,演示了Callable和Future。第1个任务返回值的和,第2个任务根据直角三角形的两条直角边长度计算斜边长度,第3个任务计算阶乘的值。这3个计算是同时进行的。

//Following are three computational threads.
import java.util.concurrent.Callable;
class Sum implements Callable<Integer> {
    int stop;
    public Sum(int stop) {
        this.stop = stop;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= stop; i++) {
            sum += i;
        }
        return sum;
    }
}
import java.util.concurrent.Callable;
class Hypot implements Callable<Double> {
    double side1,side2;

    public Hypot(double side1, double side2) {
        this.side1 = side1;
        this.side2 = side2;
    }
    @Override
    public Double call() throws Exception {
        return Math.sqrt((side1*side1)+(side2*side2));
    }
}
import java.util.concurrent.Callable;
class Factorial implements Callable<Integer> {
    int stop;
    public Factorial(int stop) {
        this.stop = stop;
    }
    @Override
    public Integer call() throws Exception {
        int fact=1;
        for(int i=2;i<=stop;i++){
            fact *= i;
        }
        return fact;
    }
}
//An example that uses a Callable.
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class CallableDemo {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(3);
        Future<Integer> f;
        Future<Double> f2;
        Future<Integer> f3;
        System.out.println("Starting");
        f = es.submit(new Sum(10));
        f2 = es.submit(new Hypot(3, 4));
        f3 = es.submit(new Factorial(5));
        try {
            System.out.println(f.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        es.shutdown();
        System.out.println("Done");
    }
    /**
     * 输出:
     * Starting
     * 55
     * 5.0
     * 120
     * Done
     */
}

5 TimeUnit枚举

   并发API定义了一些方法,这些方法的参数是TimeUnit类型,用于指明超时时间。TimeUnit是用于指定计时单位(或称为粒度)的枚举。TimeUnit是在java.util.concurrent包中定义的,可以是下列值之一:

DAYS
HOURS
MINUTES
SECONDS
MICROSECONDS
MilliSECONDS
NANOSECONDS

   尽管在调用接收计时参数的方法时,TimeUnit允许指定上述任何值,但无法保证系统能够达到指定的粒度。
   下面是一个使用TimeUnit的例子。该例对4.2节中显示的CallableDemo类进行了修改,从而使用第2种形式的get()方法,这种形式带有TimeUnit类型参数,如下所示:

try {
      System.out.println(f.get(10, TimeUnit.MILLISECONDS));
      System.out.println(f2.get(10,TimeUnit.MILLISECONDS));
      System.out.println(f3.get(10,TimeUnit.MILLISECONDS));
  } catch (InterruptedException e) {
      e.printStackTrace();
  } catch (ExecutionException e) {
      e.printStackTrace();
  } catch (TimeoutException e) {
      e.printStackTrace();
  }

   在这个版本中,所有get()方法调用等待的时间都不会超过10毫秒。
   TimeUnit枚举定义了在单位之间进行转换的各种方法,这些方法如下所示:

long convert(long tval,TimeUnit tu)
long toMicros(long tval)
long toNanos(long tval)
long toSeconds(long tval)
long toDays(long tval)
long toHours(long tval)
long toMinutes(long tval)

   convert()方法将tval转换成指定的单位并返回结果,to系列方法执行指定的转换并返回结果。
   TimeUnit枚举还定义了以下计时方法:

void sleep(long delay) throws InterruptedException
void timedJoin(Thread thrd,long delay) throws InterruptedExecution
void timedWait(Object obj,long delay) throws InterruptedExecution

   其中,sleep()方法暂停执行指定的延迟时间,延迟时间是根据调用枚举常量指定的,sleep()方法调用会被转换成Thread.sleep()调用。timedJoin()方法是Thread.join()的特殊版本,其中,thrd暂停由delay指定的时间隔间,时间间隔是根据调用时间单位描述的。timedWait()方法是Object.wait()的特殊版本,其中,obj等待由delay指定的时间间隔,时间间隔是根据调用时间单位描述。

6 并发集合

   如前所述,并发API定义了一些针对并发操作进行设计的集合类,它们包括:

ArrayBlockingQueue
ConcurrentHashMap
ConcurrentLinkedDeque
ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentSkipListSet
CopyOnWriteArrayList
CopyOnWriteArraySet
DelayQueue
LinkedBlockingDeque
LinkedBlockingQueue
LinkedTransferQueue
PriorityBlockingQueue
SynchronizedQueue

   它们是集合框架定义的相关类的并发替代版本。这些集合除了并发支持外,它们的工作方式与其他集合类似。

7 锁

   java.util.concurrent.locks包对锁提供了支持,锁是一些对象,它们为使用synchronized控制对共享资源的访问提供了替代技术。大体而言,锁的工作原理如下:在访问共享资源之前申请用于保护资源的锁;当资源访问完成时,释放锁。当某个线程正在使用锁时,如果另一个线程尝试申请锁,那么后者将会被挂起,直到锁被释放为止。通过这种方式,以防止对共享资源的冲突访问。
   当多个线程需要访问共享数据时,锁特别有用。例如,库存应用程序可能具有这样一种线程,这种线程首先确认仓库中的货物,然后在每次销售之后减少货物的数量。当两个或多个线程正在运行时,如果不采取某些形式的同步机制,那么当其中一个线程正在进行交易处理时,另一个线程也开始进行交易处理。结果可能是两个线程都认为库存充足,是当前的库存只能满足一次销售。在这种强开下,锁提供了一种机制来实现所需的同步。
   所有锁都实现了Lock接口。表2列出了Lock接口定义的方法。一般而言,ile申请锁,可以调用lock()方法。如果锁不可得,lock()方法会等待。为了释放锁,可以调用unlock()方法。为了查看锁是否可得,并且如果可得的话,就申请锁,可以调用tryLock()方法。如果锁不可得的话,tryLock()方法不会进行等待。相反,过申请到锁,就返回true;否则返回false。newCondition()方法返回一个与锁关联的Condition对象。使用Condition对象,可以通过await()以及signal()这类方法,详细地控制锁,这些方法提供了与Object.wait()和Object.notify()方法类似的功能。

表2 Lock接口定义的方法
方 法 描 述
void lock() 进行等待,直到可以获得锁为止
void lockInterruptibly() throws InteruptedException 除非被中断,否则进行等待,直到可以获得调用锁为止
Condition newCondition() 返回与调用锁关联的Condition对象
boolean tryLock() 尝试获得锁。如果锁不可得,这个方法不会等待;如果已获得锁,就返回true;如果锁当前正被另一个线程使用,就返回false
boolean tryLock(long wait,TimeUnit tu) throws InterruptedException 尝试获取锁。如果锁不可得,该方法等待的时间不会超过由wait指定的时间长度,时间单位为tu;如果已获得锁,就返回true;如果在指定的时间内无法获得锁,就返回false
void unlock() 释放锁

   java.util.concurrent.locks包提供了Lock接口的一个实现,名为ReentrantLock。ReentrantLock实现了一种可重入锁(reentrant lock),当前持有锁的线程能够重复进入这种锁。当然,对于线程重入锁的这种情况,所有lock()调用必须用相同数量的unlock()调用进行抵消。否则,试图申请锁的线程会被挂起,直到锁不再被使用为止。
   下面的程序演示了锁的使用。该程序创建两个访问共享资源Shared.count的线程。每个线程在可以访问Shared.count之前,必须获得锁。其中一个线程在获得锁之后,递增Shared.count,然后在释放锁之前,使该线程睡眠。

//A shared resource.
class Shared {
    static  int count = 0;
}
//A thread of execution that increments count.
import java.util.concurrent.locks.ReentrantLock;
class LockThread implements Runnable{
    String name;
    ReentrantLock lock;
  LockThread(ReentrantLock lk,String n){
      lock=lk;
      name = n;
      new Thread(this).start();
  }
    @Override
    public void run() {
        System.out.println("Starting "+name);
        try{
            //First,lock count.
            System.out.println(name+" is waiting to lock count.");
            lock.lock();
            System.out.println(name+" is locking count.");
            Shared.count++;
            System.out.println(name+": "+Shared.count);
            //Now,allow a context switch -- if possible.
            System.out.println(name+" is sleeping.");
            Thread.sleep(1000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //Unlock
            System.out.println(name+" is unlocking count.");
            lock.unlock();
        }
    }
}
//A simple lock example.
import java.util.concurrent.locks.ReentrantLock;
class LockDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new LockThread(lock, "A");
        new LockThread(lock, "B");
        /**
         * Starting A
         * A is waiting to lock count.
         * A is locking count.
         * A: 1
         * A is sleeping.
         * Starting B
         * B is waiting to lock count.
         * A is unlocking count.
         * B is locking count.
         * B: 2
         * B is sleeping.
         * B is unlocking count.
         */
    }
}

   java.util.concurrent.locks包还定义了ReadWriteLock接口。由这个接口指定的锁,可以读写访问维护独立的锁。这使得可以为资源读取者提供多个锁,只有资源不是正在写入即可。ReentrantReadWriteLock提供了ReadWriteLock接口的一个实现。
   注意
   JDK8添加了一个特殊的锁StampedLock。它没有实现Lock或ReadWriteLock接口,但却提供了一种机制,让自己再某些地方使用与Lock或ReadWriteLock类似。

8 原子操作

   当读取或写入某些类型的变量时,java.util.concurrent.atomic包为其他同步特性提供了替代方案。这个包提供了一些以一种不可中断的操作(即原子操作),获取、设置以及比较变量值的方法。这意味着不需要锁以及其他同步机制。
   原子操作是通过使用一些类以及方法完成的,例如AtomicInteger和AtomicLong类,以及get()、set()、compareAndSet()、decrementAndGet()和getAndSet()这类方法,它们的名称表明了它们将要执行的动作。
   下面的例子演示了如何使用AtomicInteger同步共享整数的访问:

//A shared resource.
import java.util.concurrent.atomic.AtomicInteger;
class Shared {
    static AtomicInteger ai = new AtomicInteger(0);
}
//A thread of execution that increments count.
class AtomThread implements  Runnable{
    String name;
    AtomThread(String n){
        name=n;
        new Thread(this).start();
    }
    @Override
    public void run() {
        System.out.println("Starting "+name);
        for (int i=1;i<=3;i++){
            Shared.ai.getAndSet(i);
        }
    }
}
//A simple example of Atomic.
class AtomicDemo {
    public static void main(String[] args) {
        new AtomThread("A");
        new AtomThread("B");
        new AtomThread("C");
    }
}

   在这个过程中,Shared类创建了一个静态的AtomicInteger对象,名为ai。然后,创建了3个AtomThread类型的线程。在run()方法中,调用getAndSet()方法以修改Shared.ai。getAndSet()方法将值设为参数传递的值,并返回之前的值。使用AtomicInteger可以防止两个线程同时写ai。
   一般而言,对于只涉及单个变量的情况,原子操作为其他机制提供了一种方便的替代方案(并且有可能更高效)。从JDK8开始,java.util.concurrent.atomic还提供了4个支持无锁累加操作的类,分别是DoubleAccumulator、DoubleAdder、LongAccumular和LongAdder。累加器类支持一系列用户定义的操作。加法器则维护累加和。

9 通过Fork/Join框架进行并行编程

   并行编程通常是某种技术的代名词,这种技术可以利用包含两个或更多个处理器(多核)计算机。多处理器环境的优势是能够显著提高程序的性能。JDK7增加了一些支持并行编程的类和接口。它们通常被称作Fork/Join框架,是在java.util.concurrent包中定义的。
   Fork/Join框架通过两种方式增强了多线程编程。首先,Fork/Join框架简化了多线程的创建和使用;其次,Fork/Join框架自动使用多处理器。换句话说,通过使用Fork/Join框架,应用程序能够自动伸缩,以利用可用数量的处理器。如果希望进行并行编程,这两个特性使得Fork/Join框架成为推荐使用的最佳选择。
   传统的多线程编程与并行编程之间的区别:在过去,大多数计算机只有单个CPU,多线程编程主要用于利用空闲时间,例如当程序等待用户输入时,使用这种方式,可以在某个线程空闲时执行另一个线程。换句话说,在单核CPU系统中,多线程编程用于允许允许两个或多个任务共享CPU。这种类型的多线程编程通常是通过Thread类型的对象得以支持的。尽管这种类型的多线程编程总是很有用的,但并没有针对可以使用两个或更多个CPU(多核系统)的情况进行优化。
   当具有多个CPU时,需要支持真正并行运行的第二种类型的多线程编程。使用两个或更多个CPU,可以同时执行程序的各个部分,每一部分在自己的CPU上执行,从而可以显著提升某些类型操作的执行速度,例如排序、变换或搜索大型数组。在许多情况下,这些类型的操作可以被分隔成更小的片,每一片操作数据的一部分,并且每片可以在自己的CPU上运行。得到的效率提升是巨大的。

9.1 主要的Fork/Join类

   Fork/Join框架位于java.util.concurrent包中。Fork/Join框架中处于核心地位的是以下4个类:

  • ForkJoinTask<V>:用来定义任务的抽象类。
  • ForkJoinPool:管理ForkJoinTask的执行。
  • RecursiveAction:ForkJoinTask<V>的子类,用于不返回值的任务。
  • RecursiveTask<V>:ForkJoinTask<V>的子类,用于返回值的任务。
       下面是它们之间的关系:ForkJoinPool管理ForkJoinTask的执行,ForkJoinTask是抽象类,另外两个抽象类——RecursiveAction和RecursiveTask对ForkJoinTask进行了扩展。通常,代码会扩展这些类以创建任务。
       注意
       JDK8新增的CountedCompleter类也扩展了ForkJoinTask。
       1.ForkJoinTask<V>
       ForkJoinTask<V>是抽象类,用来定义能够被ForkJoinPool管理的任务。类型参数V指定了任务的结果类型。ForkJoinTask与Thread不同,ForkJoinTask表示任务的轻量级抽象,而不是执行线程。ForkJoinTask通过线程(由ForkJoinPool类型的线程池负责管理)来执行。通过这种机制,可以使用少量的实际线程来管理大量的任务。因此与线程相比,ForkJoinTask非常高效。
       ForkJoinTask定义了许多方法,核心方法是fork()和join(),如下所示:
final ForkJoinTask<V> fork()
final V join()

   fork()方法为调用任务的异步执行提交调用任务,这意味着调用fork()方法的线程持续运行。fork()方法在调度好任务的执行后返回this。在JDK8之前,只能从另一个ForkJoinTask的计算部分的内部执行fork()方法。但是,在JDK8中,
如果没有在ForkJoinPool中执行时调用fork()方法,则会自动使用一个公共池。join()方法等待调用该方法的任务终止,任务结果被返回。因此,通过使用fork()和join()方法,可以开始一个或多个新任务,然后等待它们结束。
   ForkJoinTask的另一个重要方法是invoke()。该方法将并行(fork)和连接(join)操作合并到单个调用中,因为可以开始一个任务,并等待该任务结束。该方法如下所示:

final V invoke()

   该方法返回调用任务的结果。
   通过使用invokeAll()方法可以同时调用多个任务,该方法的两种形式入下所示:

static void invokeAll(ForkJoinTask<?> taskA,ForkJoinTask<?> taskB)
static void invokeAll(ForkJoinTask<?>...taskList)

   在第1种形式中,执行taskA和taskB。在2种形式中,执行所有指定的任务。在两种形式中,调用线程都会等待所有指定任务结束。在JDK8之前,只能从另一个ForkJoinFork的计算部分的内部执行invokeAll()方法,ForkJoinTask在FoolJoinPool中运行。JDK8包含的公共池放松了这种要求。
   2.RecursiveAction
   ForkJoinTask的其中一个子类是RecursiveAction,这个类用于封装不返回结果的任务。通常,我们的代码会扩展RecursiveAction创建具有void返回类型的任务。RecursiveAction定义了4个方法,但是通常只对其中的一个方法感兴趣:抽象方法compute()。当扩展RecursiveAction以创建具体类时,会将定义任务的代码防止compute()方法中。cumpute()方法代表任务的计算部分。
   RecursiveAction定义的compute()方法如下所示:

protected abstract void compute()

   注意,compute()是受保护的和抽象的,所以必须被子类实现(除非子类也是抽象的)。
   一般来说,RecursiveAction用于为不返回结果的任务实现递归的、分而治之的策略。
   3.RecursiveTask<V>
   ForkJoinTask的另外一个子类是RecursiveTask<V>,这个类用于封装返回结果的任务,结果类型是由V指定的。通常,我们的代码会扩展RecursiveTask<V>以创建返回值的任务。与RecursiveAction类似,该类也定义4个方法,但是通常只使用compute()抽象方法,该方法表示任务的计算部分。当扩展RecursiveTask<V>以创建具体类时,将定义任务的代码放到compute()方法中。这些代码还必须返回任务结果。
   RecursiveTask<V>定义的compute()方法如下所示:

protected abstract V compute()

   注意,compute()是受保护的和抽象的,以必须被子类实现。昂实现该方法时,必须返回任务结果。
   一般来说,RecursiveTask用于为返回结果的任务实现递归的、分而治之的策略。
   4.ForkJoinPool
   ForkJoinTask的执行发生在ForkJoinPool类中,该类还管理任务的执行。所以,为了执行ForkJoinTask,首先必须有ForkJoinPool对象。从JDK8开始,有两种方式可获得ForkJoinPool对象。首先,可以使用ForkJoinPool构造函数显示创建一个。其次,可以使用所谓公共池。JDK8新增的公共池是一个静态的ForkJoinPool对象,可供程序员使用。这里将分别介绍这两种方式。
   ForkJoinPool类定义了几个构造函数,下面是其中两个常用的构造函数:

ForkJoinPool()
ForkJoinPool(int pLevel)

   第1个构造函数创建默认池,支持的并行级别等于系统中可用处理器的数量。第2个构造函数可以指定并行级别,值必须大于0并且不能超过实现的限制。并行级别决定了能够并行执行的线程的数量。因此,并行级别实际决定了能够同时执行的任务的数量(当然,能够同时执行的任务的数量不可能超过处理器的数量)。但是,并行级别没有限制线程池能够管理的任务的数量。ForkJoinPool能够管理大大超过其并行级别热任务数。此外,并行级别只是目标,而不是确保的结果。
   创建好ForkJoinPool实例后,就可以通过大量不同的方法开始执行任务。第一个任务通常被认为是主任务。通常,由主任务开始其他的由池管理的子任务。开始主任务的一种常用方式是对ForkJoinPool实例调用invoke()方法。该方法如下所示:

<T> T invoke(ForkJoinTask<T> task)

   这个方法开始由task指定的任务,并且返回任务结果。这意味着调用代码会进行等待,直到invoke()方法返回为止。
   为了开始不用等待完成的任务,可以使用execute()方法,下面是该方法的一种形式:

void execute(ForkJoinTask<?> task)

   对于这个方法,会开始由task指定的任务,但是调用代码不会等待任务完成。相反,调用代码将继续异步执行。
   从JDK8开始,因为有另一个公共池可用,所以没有必要显式构建ForkJoinPool对象。一般来说,如果没有使用显式创建的池,就会自动使用公共池。通过ForkJoinPool定义的commonPool()方法,可以获得对公共池的引用,尽管有时候没有必须要这么做。该方法的一般形式如下所示:

static ForkJoinPool commonPool()

   该方法返回对公共池的引用。公共池提供了默认的并行级别。使用系统属性可以设置默认的并行级别。通常,对于许多应用程序来说,默认公共池启动任务。
   ForkJoinPool使用一种称为工作挪用(work stealing)的方式来管理线程的执行。每个工作者线程将从其他工作者线程获取任务。从而可以提高总效率,并且有助于维持负载均衡(因为系统中的其他进程也会占用CPU时间,所以即使两个工作者线程在它们各自的队列中具有相同的任务,也不可能在同一时间完成)。
   另外一点:ForkJoinPool使用守护线程(daemon thread)。当所有用户线程都终止时,守护线程自动终止。因此,不需要显示关闭ForkJoinPool。但是,公共池是例外,可以通过shutdown()方法关闭ForkJoinPool。shutdown()方法对公共池不起作用。

9.2 分而治之的策略

   作为通用规则,使用Fork/Join框架会用到基于递归的分而治之的策略。这就是将ForkJoinTask的两个子类称为RecursiveAction和RecursiveTask的原因。当创建自己的并行/连接任务时,可以扩展这些类中的一个。
   分而治之的策略计入如下机制:将任务地柜地划分成更多的子任务,直到子任务足够小,从而能够被连续地处理为止。例如,对于包含N个整数的数组,转换数组中每个元素的任务,可以划分成两个子任务,每个自粪污处理数组中的一半元素。也就是说,一个子任务转换0到N/2的元素,另一个子任务转换N/2到N的元素。每个子任务又可以依次被划分为另外0到N/2的元素,两一个子任务转换N/2到N的元素。每个子任务又可以依次被划分为另外一组子任务,每个子任务转换剩余元素的一半。这种划分数组的过程一直持续下去,知道达到某个临界点为止,在改临界点,连续的解决方案比进行另一次划分更快。
   分而治之的策略的优势在于处理过程可以并行发生。所以,不是使用单个线程循环遍历整个数组,而是同时处理数组的多个部分。当然,可以采用分而治之方式的许多情况往往不存在数组(或集合),但是使用这种方式的最常见情况会涉及某些类型的数组、集合或数据分组。
   使用分而治之策略的关键之一在于正确地选择临界点,在临界点使用连续处理(而不是进一步划分)。通常,最佳临界点是通过配置执行特征获得的。但是,即使使用的临界点不是最佳临界点,也能非常显著地提升速度。但是,最好避免使用过大或过小的临界点。针对ForkJoinTask<T>的Java API文档指出,根据经验,任务应当在100到10000个计算步骤之间的某个位置执行。
   另外一点:尽管在系统中可可能有多个处理器可用,但其他任务(以及操作系统自身)将会与应用程序竞争CPU资源。因此,不能假定会无限地访问所有CPU,这一点很重要。此外,因为加载的任务不同,同一程序的不同运行可能会显示不同的运行时特征。

9.3 一个简答的Fork/Join示例

   现在,看一个演示Fork/Join框架以及分而治之策略的简单例子。下面的程序将数组中的double值转换成它们的平方根。转换是通过RecursiveAction的子类完成的。注意它创建了自己的ForkJoinPool。

//A simple example of the basic divide-and-conquer strategy.
//In this case,RecursiveAction is used.
import java.util.concurrent.RecursiveAction;
//A ForkJoinTask (via RecursiveAction) that transforms
//the elements in an array of doubles into their square roots.
class SqrtTransform extends RecursiveAction {
  //The threshold value is arbitrarily set at 1,000 in this example.
  //In real-world code,its optimal value can be determined by
  //profiling and experimentation
  final int seqThreshold = 1000;
  //Array to be accessed.
  double[] data;
  //Determines what part of data to process.
  int start, end;

  public SqrtTransform(double[] data, int start, int end) {
      this.data = data;
      this.start = start;
      this.end = end;
  }

  //This is the method in which parallel computation will occur.
  @Override
  protected void compute() {
       //If number of elements is below the sequential threshold,
      //then process sequentially.
      if ((end - start) < seqThreshold) {
          //Transform each element into its square root.
          for (int i = start; i < end; i++) {
              data[i] = Math.sqrt(data[i]);
          }
      } else {
          //Otherwise,continue to break the data into smaller pieces.
          //Find the midpoint.
          int middle = (start + end) / 2;
          //Invoke new tasks,using the subdivided data.
          invokeAll(new SqrtTransform(data, start, middle), new SqrtTransform(data, middle, end));
      }
  }
}
//Demonstrate parallel execution.
import java.util.concurrent.ForkJoinPool;
class ForkJoinDemo {
   public static void main(String[] args) {
       //Create a task pool.
       ForkJoinPool fjp = new ForkJoinPool();
       double[] nums = new double[100000];
       //Give nums some values.
       for(int i=0;i<nums.length;i++){
           nums[i] = (double)i;
       }
       System.out.println("A portion of the original sequence:");
       for(int i=0;i<10;i++){
           System.out.print(nums[i]+" ");
       }
       System.out.println("\n");
       SqrtTransform task = new SqrtTransform(nums,0,nums.length);
       //Start the main ForkJoinTask.
       fjp.invoke(task);
       System.out.println("A portion of the transformed sequence" +
               " (to four decimal places):");
       for (int i=0;i<10;i++){
           System.out.format("%.4f ",nums[i]);
       }
       /**
        * 输出:
        * A portion of the original sequence:
        * 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0
        *
        * A portion of the transformed sequence (to four decimal places):
        * 0.0000 1.0000 1.4142 1.7321 2.0000 2.2361 2.4495 2.6458 2.8284 3.0000
        */
   }
}

   可以看出,数组元素的值已经被替换成它们各自的平方根。
   下面详细分析这个程序的工作原理。首先,注意SqrtTransform扩展了RecursiveAction。如前所述,RecursiveAction针对那些不返回结果的任务扩展了ForkJoinTask。接下来,注意final变量seqThreshold,这是决定何时进行连续处理的临界点的值。这个值被设置为1000(在f某种程度上,这有些随意)。接下来,注意将要处理的对数组我的引用被保存于data中,并且使用start和end域变量指示将要访问的元素的边界。
   程序的主要动作发生于compute()方法中。首先检查将要处理的元素数量是小于连续处理的临界点。如果是,就处理这些元素(此例中,是通过计算它们的平方根了来进行处理)。如果还没有达到连续处理的临界点,就通过调用invokeAll()方法开始两个新任务。本例中,每个子任务处理一半元素。如前所述,invokeAll()方法会进行等待,直到这两个任务返回为止。在所有递归调用都展开后,数组中的每个元素将已经修改过,对数组元素的修改是通过并行发生的许多操作完成的(如果有多个处理器可用的话)。
   如前所述,从JDK8开始,由于存在公共池,因此不必显式构建一个ForkJoinPool,而且使用公共池非常简单。例如,通过调用由ForkJoinPool定义的静态commonPool()方法,可以获得对公共池的引用。因此,通过调用由ForkJoinPool定义的静态commonPool()方法,可以获得对公共池的引用。因此,使用commonPool()方法调用替换调对ForkJoinPool构造函数的调用,可以把前面的程序改为是使用公共池,如下所示:

ForkJoinPool fjp = ForkJoinPool.commonPool();

   另外一种方法是,不需要显式获取对公共池的引用,因为对不在池中的任务调用ForkJoinTask的invoke()或fork()方法将自动使任务在公共池中执行。例如,在前面的程序中,可以完全删除fjp变量,而使用下面的语句启动任务:

task.invoke();

   公共池是JDK8对Fork/Join框架所做的增强之一,使其更易于使用。而且很多时候,加入不需要与JDK7兼容,公共池就是首选方法。

9.4 理解并行级别带来的影响

   在前面的例子中,使用了默认并行级别。但是,可以根据需要修改并行级别。一种方式是,当使用下面这个构造函数创建ForkJoinPool对象时指定并行级别:

ForkJoinPool(int pLevel)

   在此,pLevel指定了并行级别,必须大于0并且小于设备定义的界限。
   下面的程序创建了一个转换double数据的并行/连接任务。转换是任意的,但是被设计成使用多个CPU时钟。这样做的目的是确保能够更加清晰地显示修改临界值或并行级别后得到的效果。为了使用该程序,在命令行中指定临界值和并行级别。然后,程序运行任务。此外,还显示任务使用了多长时间。为此,使用System.nanoTime(),该方法返回JVM的高精度计时器的值。

//A simple program that lets you experiment with effects of
//changing the threshold and parallelism of a ForkJoinTask.
import java.util.concurrent.RecursiveAction;
//A ForkJoinTask (via RecursiveAction) that performs a
//transform on the elements of an array of doubles.
class Transform extends RecursiveAction {
 //Sequential threshold,which is set by the constructor.
 int seqThreshold;
 //Array to be accessed.
 double[] data;
 //Determines what part of data to process.
 int start,end;

 public Transform(double[] data, int start, int end,int t) {
     seqThreshold = t;
     this.data = data;
     this.start = start;
     this.end = end;
 }
//This is the method in which parallel computation will occur.
 @Override
 protected void compute() {
//If number of elements is below the sequential threshold,
     //then process sequentially.
     if((end-start)<seqThreshold){
         //The following code assigns an element at an even index the
         //square root of its original value.An element at an odd
         //index is assigned its cube root.This code is designed
         //to simply consume CPU time so that the effects to concurrent
         //execution are more readily observable.
         for (int i=start;i<end;i++){
             if((data[i]%2)==0){
                 data[i] = Math.sqrt(data[i]);
             }else{
                 data[i] = Math.cbrt(data[i]);
             }
         }
     }else{
         //Otherwise,continue to break the data into smaller pieces.
         //Find the midpoint.
         int middle = (start+end)/2;
         //Invoke new tasks,using the subdivided data.
         invokeAll(new Transform(data,start,middle,seqThreshold),
                 new Transform(data,middle,end,seqThreshold));
     }
 }
}
//Demonstrate parallel execution.
import java.util.concurrent.ForkJoinPool;
class FJExperiment {
 public static void main(String[] args) {
     int pLevel;
     int threshold;
     if(args.length!=2){
         System.out.println("Usage: FJExperiment parallelism threshold ");
         return ;
     }
     pLevel = Integer.parseInt(args[0]);
     threshold = Integer.parseInt(args[1]);
     //These variables are used to time the task.
     long beginT,endT;
     //Create a task pool.Notice that the parallelism level is set.
     ForkJoinPool fjp = new ForkJoinPool(pLevel);
     double[] nums = new double[1000000];
     for(int i=0;i<nums.length;i++){
         nums[i]=(double)i;
     }
     Transform task = new Transform(nums,0,nums.length,threshold);
     //Starting timing.
     beginT=System.nanoTime();
     //End timing.
     endT= System.nanoTime();
     System.out.println("Level of parallelism: "+pLevel);
     System.out.println("Sequential threshold: "+threshold);
     System.out.println("Elapsed time: "+(endT-beginT)+" ns");
 }
}

   为了使用该程序,需要指定并行级别和临界限制。应当为并行级别和临界值使用不同的数值进行试验,并观察结果。为了得到试验效果,必须在具有至少两个处理器的计算机上运行该程序。此外,还要理解两次不同的运行可能(几乎肯定)会产生不同的结果,因为系统中的其他进程会影响CPU时间的使用。
   为了通过这个试验认识不同并行级别的区别,首先,想下面这样执行程序:

java FJExperiment 1 1000

   这个命令要求并行级别为1(在本质上也就是顺序执行),临界值为1000。在双核计算机上运行,产生的一次样本结果如下所示:

Level of parallelism: 1
Sequential threshold: 1000
Elapsed time: 259677487 ns

   现在,像下面这样将并行级别指定为2:

java FJExperiment 2 1000

   下面是在相同的双核计算机运行该程序后产生的一次样本结果:

Level of parallelism: 2
Sequential threshold: 1000
Elapsed time: 169254472 ns

   正如运行结果所证实的,增加并行级别后明显减少了执行时间,因此提高了程序的速度。
   当试验并行/连接程序的执行特征时,
还有两个方法可能会有用。首先,可以通过调用getParallelism()方法获取并行级别,该方法是由ForkJoinPool定义的,如下所示:

int getParallelism()

   该方法返回当前起作用的并行级别。对于创建的池,默认并行级别等于可用处理器的数量。要获取公共池的并行级别,也可以使用JDK8新增的getCommonPoolParallelism()方法。其次,可以通过调用avaliableProcessors()方法来获取系统中可用于处理器的数量,该方法是由Runtime类定义的,如下所示:

int availableProcessors()

   缘于其他系统要求,一次调用的返回值可能与下一次调用的返回值不同。

9.5 一个使用RecursiveTask<V>的例子

   前两个例子是基于RecursiveAction构建的,这意味着它们可以并行执行不返回结果的任务。为了创建返回结果的任务,需要使用RecursiveTask。一般来说,设计解决方案的方式与刚才显示的相同。关键区别在于compute()方法会返回结果。因此,必须累加结果,从而当第一个调用结束时,返回整个结果。另一个区别在于,通常是通过显示地调用fork()和join()方法来开始子任务(例如,不是通过调用invokeAll()方法来隐式启动子任务)。
   下面的程序演示了RecursiveTask。该程序创建一个名为Sum的任务,该任务返回double数组中数值的总和。在这个例子中,数组包含5000个元素。但是,每隔一个元素是负值。因此在该数组中,前面一部分元素的值是0、-1、2、-3、4,等等(为使这个例子在JDK7和JDK8中都可以工作,让它创建自己的池。)

//A simple example that uses RecursiveTask<V>.
//A RecursiveTask that computes the summation of an array of doubles.
import java.util.concurrent.RecursiveTask;
class Sum extends RecursiveTask<Double> {
  //The sequential threshold value.
  final int seqThresHold=500;
  //Array to be accessed.
  double[] data;
  //Determines what part of data to process.
  int start,end;

  public Sum(double[] data, int start, int end) {
      this.data = data;
      this.start = start;
      this.end = end;
  }
//Find the summation of an array of doubles.
  @Override
  protected Double compute() {
      double sum =0;
      //If number of elements is below the sequential threshold,
      //then process sequentially.
      if((end-start)<seqThresHold){
          //Sum the elements.
          for(int i=start;i<end;i++){
              sum+=data[i];
          }
      }else{
          //Otherwise,continue to break the data into smaller pieces.
          //Find the midpoint.
          int middle= (start+end)/2;
          //Invoke new tasks,using the subdivided data.
          Sum subTaskA = new Sum(data,start,middle);
          Sum subTaskB = new Sum(data,middle,end);
          //Start each subtask by forking.
          subTaskA.fork();
          subTaskB.fork();
          //Wait for the subtasks to return,and aggregate the results.
          sum = subTaskA.join()+subTaskB.join();
      }
      //Return the final sum.
      return sum;
  }
}
//Demonstrate parallel execution.
import java.util.concurrent.ForkJoinPool;
class RecurTaskDemo {
  public static void main(String[] args) {
      //Create a task pool.
      ForkJoinPool fjp = new ForkJoinPool();
      double[] nums = new double[5000];
      //Initialize nums with values that alternate between
      //positive and negative.
      for (int i=0;i<nums.length;i++){
          nums[i]=(double)(((i%2)==0)?i:-i);
      }
      Sum task = new Sum(nums,0,nums.length);
      //Start the ForkJoinTasks.Notice that,int this case,
      //invoke() returns a result.
      double summation=fjp.invoke(task);
      System.out.println("Summation "+summation);
      /**
       * 输出:
       * Summation -2500.0
       */
  }
}

   在该程序中,首先,注意两个子任务是通过调用fork()方法来执行的,如下所示:

subTaskA.fork();
subTaskB.fork();

   在这个例子中,之所以使用fork()方法,是因为该方法启动任务却不等待任务结束(因此能异步地运行任务)。通过调用join()方法可以获取每个任务的结果,如下所示:

sum = subTaskA.join() + subTaskB.join();

   这条语句会进行等待,直到每个任务完成为止。然后将每个任务的结果相加,并将综合赋给sum。因此,每个子任务的总和被加到运行总和。最后,通过返回sum结束compute()方法的运行,sum是当第一次调用返回时的最后总和。
   还有另外两种方式可以用于处理子任务的异步执行。例如,下面的语句使用fork()方法启动subTaskA,使用invoke()方法启动并等待subTaskB:

subTaskA.fork();
sum = subTaskB.invoke() + subTaskA.join();

   另外一种方式是直接为subTaskB调用compute()方法,如下所示:

subTask.fork();
sum = subTaskB.compute() + subTaskA.join();
9.6 异步执行任务

   前面的程序在ForkJoinPool中调用invoke()方法以开始任务。当调用线程必须等待任务完成时,通常使用这种方式,因为直到任务终止,invoke()方法才返回,但是,可以异步地开始任务。对于这种方式,调用线程继续执行。因此,调用线程和任务同时执行。为了异步地开始任务,需要使用execute()方法,该方法也是由ForkJoinPool定义的,具有如下所示的两种方式:

void execute(ForkJoinTask<?> task)
void execute(Runnable task)

   在这两种形式中,task指定了要运行的任务。注意第2种形式允许指定Runnable对象,而不是ForkJoinTask任务。因此,这种形式桥接了Java传统的多线程编程方式的新的Fork/Join框架。ForkJoinPool使用的线程是守护线程。因此,当主线程结束时,它们也会结束。所以,可能需要保持主线程活跃,知道任务结束。

9.7 取消任务

   可以通过调用cancel()方法来取消任务,该方法是由ForkJoinTask定义的,一般形式如下所示:

boolean cancel(boolean interruptOK)

   如果调用该方法的任务被取消,就返回true;如果任务已经结束或者不能取消,就返回false。
   可以通过调用isCancelled()方法来确定任务是否已经取消,该方法如下所示:

final boolean isCancelled()

   如果调用任务在结束之前已经取消,就返回true;否则返回false。

9.8 确定任务的完成状态

   除了刚才描述的isCancelled()方法之外,ForkJoinTask还包含另外两种方法,可以使用它们来确定任务的完成状态。第1个是isCompletedNormally()方法,该方法如下所示:

final boolean isCompleteNormally()

   如果调用任务正常结束,即没有抛出异常,并且也没有通过cancel()方法调用来取消,就返回true;否则返回false。
   第2个是isCompletedAbnormally()方法,该方法如下所示:

final boolean isCompleteAbnormally()

   如果调用任务是因为取消或因为抛出异常而完成的,就返回true;否则返回false。

9.9 重新启动任务

   正常情况下,不能重新运行任务。任务一旦完成,就不能重新启动。但是,(在任务完成后)可以重新初始化任务的状态,从而使其能够再次运行。这是通过调用reinitialize()方法来完成的,该方法如下所示:

void reinitialize()

   这个方法重置调用任务的状态。但是,对于由任务操作的任何永久数据来说,已经做的任何修改都不会被取消。例如,如果任务修改了某个数组,那么调用reinitialize()方法不会取消那些修改。

9.10 深入研究

   1、其他的ForkJoinTask特性举例
   有些时候,希望确保invokeAll()和fork()这样的方法只在ForkJoinTask中调用(使用JDK7时这一点可能尤为重要,因为JDK7不支持公共池)。遵守这条规则通常很容易,但是在有些情况下,可能具有能够在任务内部或外部执行的代码,可以通过inForkJoinPool()方法,确定代码是否在任务内部执行。
   可以通过ForkJoinTask定义的adapt()方法,将Runnable或Callable对象转换成ForkJoinTask对象。该方法有3种形式,一种用于装换Callable对象,另一种用于装换不返回结果的Runnable对象,最后一种用于返回结果的Runnable对象。对于Callable对象,运行call()方法,对于Runnable对象,运行run()方法。
   可以通过调用getQueuedTaskCount()方法,获取调用线程的队列中任务的大概数量。可以通过调用getSurplusQueuedTaskCount()方法,获取调用线程的队列中任务数量超出池中其他线程任务数量的大约数目,其他线程可能会"挪用"多出的这些任务。在Fork/Join框架中,任务挪用是获得高效率的一种方式。尽管这个过程是自动进行的,但在某些情况下,对于优化吞吐量,这一信息可能是有用的。
   ForkJoinTask定义了以quietly作为前缀的join()和invoke()方法,它们如下所示:

  • final void quietlyJoin(): 连接任务,但是不返回结果,也抛出异常
  • final coid quietlyInvoke(): 调用任务,但是不返回结果,也不抛出异常。
       本质上,除了不返回值以及不抛出异常外,这两个方法与它们对象的没有quiet前缀的方法相似。
       可以通过调用tryUnlock()方法,尝试"不调用"任务。
       JDK8新增了几个支持标记的方法,如getForkJoinTask()和setForkJoinTaskTag()。标记是链接到任务的短整数值。在特殊的应用程序中,它们可能十分有用。
       ForkJoinTask实现了Serializable接口,因而能够被串行化。但是,在执行期间不能应用串行化。
       2、其他的ForkJoinPool特性举例
       当调试并行/连接应用程序时,有一个方法特别有用——ForkJoinPool重写的toString()方法。该方法显示"用户友好的"池状态概要信息。为了查看toString()方法的使用情况,在前面的试验程序FJExperiment类中,使用下面的语句启动并等待任务。
//Asynchronously start the main ForkJoinTask.
fjp.execute(task);
//Display the state of pool while waiting.
while(!task.isDone()){
   System.out.println(fjp);
}

   当运行程序时,在屏幕上会看到一些了描述池状态的消息。下面是一个例子。当然,根据处理器的数量、临界点的值、任务负载等,输出可能会与此不同。

java.util.concurrent.ForkJoinPool@141d683[Running,paralelism=2,size=2,active=0,running=2,steals=0,task2=0,submissions=1]

   通过调用isQuiescent()方法,可以确定池当前是否空闲。如果池中没有活动的线程,就返回true;否则返回false。
   通过调用getPoolSize()方法,可以获得池中当前是否空闲。如果池中没有活动的线程,就返回true;否则返回false。
   为了关闭池,可以调用shutdown()方法。当前活动的任务仍然会执行,但是不会启动新的任务。为了立即停止池,可以调用shutdownNow()方法。在这种情况下,会尝试取消当前活动的线程数(但是必须注意,这两个方法不会影响公共池)。通过调用isShutdown()方法,可以确定池是否关闭。如果池已经关闭,就返回true;否则返回false。为了确定池是否已经关闭并且确定所有任务是否已经完成,可以调用isTerminates()方法。

发布了59 篇原创文章 · 获赞 20 · 访问量 3627

猜你喜欢

转载自blog.csdn.net/qq_34896730/article/details/104511616