Java-线程和并发工具教程-三-

108 阅读42分钟

Java 线程和并发工具教程(三)

原文:JJava Threads and the Concurrency Utilities

协议:CC BY-NC-SA 4.0

八、其他并发工具

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1700-9_​8) contains supplementary material, which is available to authorized users.

第五章到第七章向您介绍了并发工具、执行器(以及可调用和期货)、同步器和锁定框架。在这一章中,我将通过向您介绍并发集合、原子变量、Fork/Join 框架和完成服务来完成我对并发工具的介绍。

Note

由于时间不够,我也无法报道完整的期货。如果你对这个话题感兴趣,我建议你看看 Tomasz Nurkiewicz 在 http://www.nurkiewicz.com/2013/05/java-8-definitive-guide-to.html 发表的题为“Java 8:CompletableFuture 权威指南”的精彩博文。

并发收款

Java 的集合框架提供了位于java.util包中的接口和类。接口包括ListSetMap;课程包括ArrayListTreeSetHashMap

ArrayListTreeSetHashMap以及其他实现这些接口的类都不是线程安全的。然而,您可以通过使用位于java.util.Collections类中的同步包装方法使它们成为线程安全的。例如,您可以将一个ArrayList实例传递给Collections.synchronizedList(),以获得一个ArrayList的线程安全变体。

尽管在多线程环境中经常需要它们来简化代码,但是线程安全集合存在一些问题:

  • 在迭代一个集合之前获取一个锁是必要的,这个集合可能在迭代过程中被另一个线程修改。如果没有获得锁并且集合被修改,那么很可能会抛出java.util.ConcurrentModificationException。发生这种情况是因为集合框架类返回失败快速迭代器,这些迭代器在迭代过程中修改集合时抛出ConcurrentModificationException。失败快速迭代器对于并发应用来说通常是不方便的。
  • 当从多个线程频繁访问同步集合时,性能会受到影响。这个性能问题最终会影响应用的可伸缩性。

并发工具通过包含并发集合来解决这些问题,并发集合是存储在java.util.concurrent包中的高并发性能和高度可伸缩的面向集合的类型。它的面向集合的类返回弱一致性迭代器,这些迭代器具有以下属性:

  • 迭代开始后移除但尚未通过迭代器的next()方法返回的元素不会被返回。
  • 迭代开始后添加的元素可能会返回,也可能不会返回。
  • 在集合的迭代过程中,任何元素都不会返回多次,无论在迭代过程中对集合做了什么更改。

下面的列表提供了一个面向并发的集合类型的简短示例,您可以在java.util.concurrent包中找到:

  • BlockingQueuejava.util.Queue的一个子接口,它也支持阻塞操作,即在检索元素之前等待队列变为非空,在存储元素之前等待队列中有可用空间。每个ArrayBlockingQueueDelayQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue类都直接实现了这个接口。LinkedBlockingDequeLinkedTransferQueue类通过BlockingQueue子接口实现这个接口。
  • ConcurrentMapjava.util.Map的子接口,它声明了额外的不可分割的putIfAbsent()remove()replace()方法。ConcurrentHashMap类(并发等价于java.util.HashMap)、ConcurrentNavigableMap类和ConcurrentSkipListMap类实现了这个接口。

Oracle 的 Javadoc for BlockingQueueArrayBlockingQueue和其他面向并发的集合类型将这些类型标识为集合框架的一部分。

使用 BlockingQueue 和 ArrayBlockingQueue

BlockingQueue的 Javadoc 揭示了生产者-消费者应用的核心,它比第三章(见清单 3-1 )中所示的等效应用要简单得多,因为它不需要处理同步。清单 8-1 在高级生产者-消费者对等体中使用了BlockingQueue及其ArrayBlockingQueue实现类。

Listing 8-1. The Blocking Queue Equivalent of Chapter 3’s PC Application

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class PC

{

public static void main(String[] args)

{

final BlockingQueue<Character> bq;

bq = new ArrayBlockingQueue<Character>(26);

final ExecutorService executor = Executors.newFixedThreadPool(2);

Runnable producer = () ->

{

for (char ch = 'A'; ch <= 'Z'; ch++)

{

try

{

bq.put(ch);

System.out.printf("%c produced by " +

"producer.%n", ch);

}

catch (InterruptedException ie)

{

}

}

};

executor.execute(producer);

Runnable consumer = () ->

{

char ch = '\0';

do

{

try

{

ch = bq.take();

System.out.printf("%c consumed by " +

"consumer.%n", ch);

}

catch (InterruptedException ie)

{

}

}

while (ch != 'Z');

executor.shutdownNow();

};

executor.execute(consumer);

}

}

清单 8-1 分别使用BlockingQueueput()take()方法将一个对象放入阻塞队列和从阻塞队列中移除一个对象。put()没有地方放东西时会阻塞;take()队列为空时阻塞。

虽然BlockingQueue确保了一个角色在产生之前不会被消耗,但是这个应用的输出可能会有不同的指示。例如,下面是一次运行的部分输出:

Y consumed by consumer.

Y produced by producer.

Z consumed by consumer.

Z produced by producer.

清单 3-2 中第三章的PC应用通过在setSharedChar() / System.out.println()周围引入一个额外的同步层和在getSharedChar() / System.out.println()周围引入一个额外的同步层,克服了这种不正确的输出顺序。清单 7-2 中第七章的PC应用通过将这些方法调用放在lock() / unlock()方法调用之间,克服了这种不正确的输出顺序。

了解有关 ConcurrentHashMap 的更多信息

ConcurrentHashMap类的行为类似于HashMap,但是被设计成在多线程环境中工作,不需要显式同步。例如,您经常需要检查映射是否包含特定的值,如果没有该值,则将该值放入映射中:

if (!map.containsKey("some string-based key"))

map.put("some string-based key", "some string-based value");

尽管这段代码很简单,看起来也能完成工作,但它并不是线程安全的。在调用map.containsKey()map.put()之间,另一个线程可以插入这个条目,然后这个条目会被覆盖。要修复这种争用情况,您必须显式同步这段代码,我在这里演示了这一点:

synchronized(map)

{

if (!map.containsKey("some string-based key"))

map.put("some string-based key", "some string-based value");

}

这种方法的问题是,在检查键是否存在并在键不存在时将条目添加到映射中时,您已经锁定了整个映射的读写操作。当许多线程试图访问映射时,这种锁定会影响性能。

通用的ConcurrentHashMap<V>类通过提供V putIfAbsent(K key, V value)方法解决了这个问题,当key不存在时,该方法在地图中引入一个key / value条目。此方法等效于以下代码片段,但提供了更好的性能:

synchronized(map)

{

if (!map.containsKey(key))

return map.put(key, value);

else

return map.get(key);

}

使用putIfAbsent(),先前的代码片段被翻译成以下更简单的代码片段:

map.putIfAbsent("some string-based key", "some string-based value");

Note

Java 8 通过添加 30 多个新方法改进了ConcurrentHashMap,这些新方法主要通过聚合操作支持 lambda 表达式和 Streams API。执行聚合操作的方法包括forEach()方法(forEach()forEachKey()forEachValue()forEachEntry())、搜索方法(search()searchKeys()searchValues()searchEntries())以及归约方法(reduce()reduceToDouble()reduceToLong()等)。其他方法(如mappingCount()newKeySet())也已添加。由于 JDK 8 的改变,ConcurrentHashMap现在作为缓存更有用了。缓存改进的变化包括计算键值的方法,对扫描(也可能是驱逐)条目的改进支持,以及对包含大量元素的地图的更好支持。

原子变量

与对象监视器相关联的固有锁历来性能不佳。尽管性能有所提高,但在创建 web 服务器和其他需要高可伸缩性和高性能的应用时,在存在大量线程争用的情况下,它们仍然是一个瓶颈。

许多研究都致力于创建非阻塞算法,这些算法可以从根本上提高同步上下文中的性能。这些算法提高了可伸缩性,因为当多个线程争用相同的数据时,线程不会阻塞。此外,线程不会遇到死锁和其他活动问题。

Java 5 通过引入java.util.concurrent.atomic包提供了创建高效非阻塞算法的能力。根据这个包的 JDK 文档,java.util.concurrent.atomic提供了一个支持无锁、线程安全的单变量操作的小型工具包。

java.util.concurrent.atomic包中的类将volatile值、字段和数组元素的概念扩展到那些也提供原子条件更新的元素,因此不需要外部同步。换句话说,在没有外部同步的情况下,与volatile变量相关的内存语义是互斥的。

Note

术语原子和不可分割被广泛认为是等价的,即使我们可以分裂原子。

位于java.util.concurrent.atomic中的一些类描述如下:

  • AtomicBoolean:可以自动更新的boolean值。
  • AtomicInteger:可以自动更新的int值。
  • AtomicIntegerArray:一个int数组,它的元素可以自动更新。
  • AtomicLong:可以自动更新的long值。
  • AtomicLongArray:一个long数组,其元素可以被自动更新。
  • AtomicReference:可以自动更新的对象引用。
  • AtomicReferenceArray:一个对象引用数组,其元素可以自动更新。

原子变量用于实现计数器、序列生成器(如java.util.concurrent.ThreadLocalRandom)和其他需要互斥的构造,在高线程争用的情况下不会出现性能问题。例如,考虑列出 8-2 的ID类,其getNextID()类方法返回唯一的长整数标识符。

Listing 8-2. Returning Unique Identifiers in a Thread-Safe Manner via synchronized

class ID

{

private static volatile long nextID = 1;

static synchronized long getNextID()

{

return nextID++;

}

}

尽管代码得到了适当的同步(并且考虑到了可见性),但是在大量线程争用的情况下,与synchronized相关联的固有锁会损害性能。此外,还可能出现死锁等活性问题。清单 8-3 向您展示了如何通过用原子变量替换synchronized来避免这些问题。

Listing 8-3. Returning Unique IDs in a Thread-Safe Manner via AtomicLong

import java.util.concurrent.atomic.AtomicLong;

class ID

{

private static AtomicLong nextID = new AtomicLong(1);

static long getNextID()

{

return nextID.getAndIncrement();

}

}

在清单 8-3 中,我已经将nextIDlong转换为AtomicLong实例,并将该对象初始化为1。我还重构了getNextID()方法来调用AtomicLonggetAndIncrement()方法,该方法将AtomicLong实例的内部长整型变量递增 1,并在一个不可分割的步骤中返回前一个值。没有显式同步。

Note

java.util.concurrent.atomic包包括DoubleAccumulatorDoubleAdderLongAccumulatorLongAdder类,这些类解决了在维护单个计数、总和或其他值的环境中的可伸缩性问题,并且有可能从多个线程进行更新。这些新类“在内部采用了争用减少技术,与原子变量相比,这些技术提供了巨大的吞吐量改进。这是通过以一种在大多数应用中可接受的方式放松原子性保证而成为可能的。”

理解原子魔法

Java 的低级同步机制强制实施互斥(持有保护一组变量的锁的线程对它们具有独占访问权)和可见性(对被保护变量的更改对随后获得锁的其他线程变得可见),以下列方式影响硬件利用率和可伸缩性:

  • 争用同步(多个线程不断竞争一个锁)开销很大,结果会影响吞吐量。这种开销主要是由频繁的上下文切换(将中央处理单元从一个线程切换到另一个线程)引起的。每个上下文切换操作可能需要许多处理器周期才能完成。相比之下,现代 Java 虚拟机(JVM)使得无竞争同步变得廉价。
  • 当持有锁的线程被延迟时(例如,由于调度延迟),没有需要该锁的线程取得任何进展;硬件没有得到应有的利用。

尽管您可能认为可以使用volatile作为同步的替代方案,但这是行不通的。可变变量只能解决可见性问题。它们不能用于安全地实现原子的读-修改-写序列,而这些序列是实现线程安全计数器和其他需要互斥的实体所必需的。然而,有一个替代方案负责并发工具提供的性能增益(比如java.util.concurrent.Semaphore类)。这种替代方法被称为比较和交换。

比较和交换(CAS)是不间断微处理器专用指令的通用术语,该指令读取存储单元,将读取值与期望值进行比较,并在读取值与期望值匹配时在存储单元中存储新值。否则,什么都不做。现代微处理器提供各种 CAS。例如,英特尔微处理器提供了cmpxchg系列指令,而较老的 PowerPC 微处理器提供了等效的加载链接(如lwarx)和条件存储(如stwcx)指令。

CAS 支持原子读取-修改-写入序列。您通常按如下方式使用 CAS:

Read value x from address A.   Perform a multistep computation on x to derive a new value called y.   Use CAS to change the value of A from x to y. CAS succeeds when A’s value hasn’t changed while performing these steps.  

为了理解 CAS 的好处,考虑一下清单 8-2 的ID类,它返回一个唯一的标识符。因为这个类声明了它的getNextID()方法synchronized,对监视器锁的高度争用会导致过多的上下文切换,这会延迟所有的线程,并导致应用不能很好地伸缩。

假设存在一个在value中存储基于int的值的CAS类。此外,它还提供了返回value的原子方法int getValue()和实现 CAS 的原子方法int compareAndSwap(int expectedValue, int newValue)。(在幕后,CAS 依靠 Java 本地接口[JNI]来访问特定于微处理器的 CAS 指令。)

compareAndSwap()方法自动执行以下指令序列:

int readValue = value;            // Obtain the stored value.

if (readValue == expectedValue)   // If stored value not modified ...

value = newValue;              // ... change to new value.

return readValue;                 // Return value before a potential change.

清单 8-4 展示了一个新版本的ID,它使用CAS类以高性能的方式获得一个惟一的标识符。(忘记使用 JNI 的性能影响,假设我们可以直接访问特定于微处理器的 CAS 指令。)

Listing 8-4. Returning Unique IDs in a Thread-Safe Manner via CAS

class ID

{

private static CAS value = new CAS(1);

static long getNextID()

{

int curValue = value.getValue();

while (value.compareAndSwap(curValue, curValue + 1) != curValue)

curValue = value.getValue();

return curValue - 1;

}

}

ID封装一个初始化为int-值1CAS实例,并声明一个getNextID()方法,用于检索当前标识符值,然后在该实例的帮助下递增该值。在检索实例的当前值后,getNextID()重复调用compareAndSwap(),直到curValue的值没有改变(被另一个线程改变)。然后,该方法可以随意更改该值,之后它会返回上一个值。当不涉及锁时,可以避免争用以及过多的上下文切换。性能提高了,代码更具可伸缩性。

作为 CAS 如何改进并发工具的一个例子,考虑java.util.concurrent.locks.ReentrantLock。在高线程争用的情况下,这个类比synchronized提供了更好的性能。为了提高性能,ReentrantLock的同步由抽象java.util.concurrent.locks.AbstractQueuedSynchronizer类的一个子类来管理。反过来,这个类利用了未记录的sun.misc.Unsafe类和它的compareAndSwapInt() CAS 方法。

原子变量类也利用了 CAS。此外,它们还提供了一种具有以下形式的方法:

boolean compareAndSet(expectedValue, updateValue)

这个方法(在不同的类中参数类型不同)在当前保存expectedValue时自动地为updateValue设置一个变量,成功时报告true

分叉/连接框架

代码总是需要更快地执行。历史上,这种需求是通过提高微处理器速度和/或支持多处理器来满足的。然而,在 2003 年左右,由于自然限制,微处理器速度停止增长。为了弥补这一点,处理器制造商开始在处理器中添加多个处理内核,通过大规模并行来提高速度。

Note

并行性是指通过多个处理器和内核的某种组合同时运行线程。相比之下,并发是一种更普遍的并行形式,其中线程通过上下文切换同时运行或似乎同时运行,也称为虚拟并行。有些人进一步将并发性描述为程序或操作系统的属性,将并行性描述为同时执行多个线程的运行时行为。

Java 通过其低级线程特性和高级并发工具(如线程池)来支持并发。并发的问题是它不能最大限度地利用可用的处理器/内核资源。例如,假设您创建了一个排序算法,该算法将数组分成两半,分配两个线程对每一半进行排序,并在两个线程完成后合并结果。

让我们假设每个线程运行在不同的处理器上。因为数组的每一半中可能出现不同数量的元素重新排序,所以一个线程可能会在另一个线程之前完成,并且必须在合并发生之前等待。在这种情况下,浪费了处理器资源。

这个问题(以及代码冗长和难以阅读的相关问题)可以通过递归地将任务分解成子任务并组合结果来解决。这些子任务并行运行,并且几乎同时完成(如果不是同时完成的话),它们的结果被合并,并通过栈传递到前一层子任务。等待几乎不会浪费任何处理器时间,递归代码也不那么冗长,而且(通常)更容易理解。Java 提供了 Fork/Join 框架来实现这个场景。

Fork/Join 由一个特殊的执行器服务和线程池组成。executor 服务使一个任务对框架可用,这个任务被分解成更小的任务,这些任务从池中派生出来(由不同的线程执行)。任务会一直等待,直到被加入(其子任务完成)。

Fork/Join 使用工作窃取来最小化线程争用和开销。工作线程池中的每个工作线程都有自己的双端工作队列,并将新任务推送到该队列。它从队列的头部读取任务。如果队列为空,工作线程会尝试从另一个队列的尾部获取任务。窃取并不常见,因为工作线程按照后进先出(LIFO)的顺序将任务放入队列中,并且随着问题被分成子问题,工作项的大小会变得更小。你开始把任务交给一个中心工作人员,它继续把它们分成更小的任务。最终,所有的工人都与最小同步有关。

Fork/Join 主要由java.util.concurrent包的ForkJoinPoolForkJoinTaskForkJoinWorkerThreadRecursiveActionRecursiveTaskCountedCompleter类组成:

  • ForkJoinPool是运行ForkJoinTaskjava.util.concurrent.ExecutorService实现。一个ForkJoinPool实例为来自非ForkJoinTask客户端的提交提供入口点,并提供管理和监控操作。
  • ForkJoinTask是在ForkJoinPool上下文中运行的任务的抽象基类。一个ForkJoinTask实例是一个类似线程的实体,它比普通线程要轻得多。在一个ForkJoinPool中,大量的任务和子任务可能由少量的实际线程托管,代价是一些使用限制。
  • ForkJoinWorkerThread描述一个由ForkJoinPool实例管理的线程,它执行ForkJoinTask s
  • RecursiveAction描述一个递归的无结果ForkJoinTask
  • RecursiveTask描述了一个递归的结果承载ForkJoinTask
  • CountedCompleter描述了一个ForkJoinTask,它具有一个在被触发时执行的完成动作(完成一个 fork/join 任务的代码),并且没有剩余的挂起动作。

Java 文档提供了基于RecursiveAction的任务(比如排序)和基于RecursiveTask的任务(比如计算斐波那契数)的例子。也可以用RecursiveAction来完成矩阵乘法(见en . Wikipedia . org/wiki/Matrix _ multiplication)。例如,假设您已经创建了清单 8-5 的Matrix类来表示由特定数量的行和列组成的矩阵。

Listing 8-5. A Class for Representing a Two-Dimensional Table

public class Matrix

{

private final int[][] matrix;

public Matrix(int nrows, int ncols)

{

matrix = new int[nrows][ncols];

}

public int getCols()

{

return matrix[0].length;

}

public int getRows()

{

return matrix.length;

}

public int getValue(int row, int col)

{

return matrix[row][col];

}

public void setValue(int row, int col, int value)

{

matrix[row][col] = value;

}

}

清单 8-6 展示了将两个Matrix实例相乘的单线程方法。

Listing 8-6. Multiplying Two Matrix Instances via the Standard Matrix-Multiplication Algorithm

public class MatMult

{

public static void main(String[] args)

{

Matrix a = new Matrix(1, 3);

a.setValue(0, 0, 1); // | 1 2 3 |

a.setValue(0, 1, 2);

a.setValue(0, 2, 3);

dump(a);

Matrix b = new Matrix(3, 2);

b.setValue(0, 0, 4); // | 4 7 |

b.setValue(1, 0, 5); // | 5 8 |

b.setValue(2, 0, 6); // | 6 9 |

b.setValue(0, 1, 7);

b.setValue(1, 1, 8);

b.setValue(2, 1, 9);

dump(b);

dump(multiply(a, b));

}

public static void dump(Matrix m)

{

for (int i = 0; i < m.getRows(); i++)

{

for (int j = 0; j < m.getCols(); j++)

System.out.printf("%d ", m.getValue(i, j));

System.out.println();

}

System.out.println();

}

public static Matrix multiply(Matrix a, Matrix b)

{

if (a.getCols() != b.getRows())

throw new IllegalArgumentException("rows/columns mismatch");

Matrix result = new Matrix(a.getRows(), b.getCols());

for (int i = 0; i < a.getRows(); i++)

for (int j = 0; j < b.getCols(); j++)

for (int k = 0; k < a.getCols(); k++)

result.setValue(i, j, result.getValue(i, j) +

a.getValue(i, k) * b.getValue(k, j));

return result;

}

}

清单 8-6 的MatMult类声明了一个演示矩阵乘法的multiply()方法。在验证了第一个Matrix ( a)中的列数等于第二个Matrix ( b)中的行数(这对于算法来说是必不可少的)之后,multiply()创建一个result Matrix,并进入一系列嵌套循环来执行乘法。

这些循环的本质如下:对于a中的每一行,将该行的每一列值乘以b中相应列的行值。将乘法的结果相加,并将总数存储在通过a中的行索引(i)和b中的列索引(j)指定的位置的result中。

编译清单 8-6 和清单 8-5 ,它们必须在同一个目录中,如下所示:

javac MultMat.java

运行生成的应用,如下所示:

java MatMult

您应该观察到以下输出,它表明 1 行 3 列的矩阵乘以 3 行 2 列的矩阵得到 1 行 2 列的矩阵:

1 2 3

4 7

5 8

6 9

32 50

计算机科学家将这种算法归类为 O(nnn),读作“n 次方的 big-oh”或“近似 n 次方”。这种符号是对算法性能进行分类的一种抽象方式(不会陷入具体细节,如微处理器速度)。O(nnn)分类指示非常差的性能,并且这种性能随着被相乘的矩阵的大小增加而恶化。

通过将每个逐行逐列的乘法任务分配给单独的类似线程的实体,可以提高性能(在多处理器和/或多核平台上)。清单 8-7 向您展示了如何在 Fork/Join 框架的上下文中完成这个场景。

Listing 8-7. Multiplying Two Matrix Instances with Help from the Fork/Join Framework

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.ForkJoinPool;

import java.util.concurrent.RecursiveAction;

public class MatMult extends RecursiveAction

{

private final Matrix a, b, c;

private final int row;

public MatMult(Matrix a, Matrix b, Matrix c)

{

this(a, b, c, -1);

}

public MatMult(Matrix a, Matrix b, Matrix c, int row)

{

if (a.getCols() != b.getRows())

throw new IllegalArgumentException("rows/columns mismatch");

this.a = a;

this.b = b;

this.c = c;

this.row = row;

}

@Override

public void compute()

{

if (row == -1)

{

List<MatMult> tasks = new ArrayList<>();

for (int row = 0; row < a.getRows(); row++)

tasks.add(new MatMult(a, b, c, row));

invokeAll(tasks);

}

else

multiplyRowByColumn(a, b, c, row);

}

public static void multiplyRowByColumn(Matrix a, Matrix b, Matrix c,

int row)

{

for (int j = 0; j < b.getCols(); j++)

for (int k = 0; k < a.getCols(); k++)

c.setValue(row, j, c.getValue(row, j) +

a.getValue(row, k) * b.getValue(k, j));

}

public static void dump(Matrix m)

{

for (int i = 0; i < m.getRows(); i++)

{

for (int j = 0; j < m.getCols(); j++)

System.out.print(m.getValue(i, j) + " ");

System.out.println();

}

System.out.println();

}

public static void main(String[] args)

{

Matrix a = new Matrix(2, 3);

a.setValue(0, 0, 1); // | 1 2 3 |

a.setValue(0, 1, 2); // | 4 5 6 |

a.setValue(0, 2, 3);

a.setValue(1, 0, 4);

a.setValue(1, 1, 5);

a.setValue(1, 2, 6);

dump(a);

Matrix b = new Matrix(3, 2);

b.setValue(0, 0, 7); // | 7 1 |

b.setValue(1, 0, 8); // | 8 2 |

b.setValue(2, 0, 9); // | 9 3 |

b.setValue(0, 1, 1);

b.setValue(1, 1, 2);

b.setValue(2, 1, 3);

dump(b);

Matrix c = new Matrix(2, 2);

ForkJoinPool pool = new ForkJoinPool();

pool.invoke(new MatMult(a, b, c));

dump(c);

}

}

清单 8-7 展示了一个扩展RecursiveActionMatMult类。为了完成有意义的工作,RecursiveActionvoid compute()方法被覆盖。

Note

虽然compute()通常用于递归地将任务细分为子任务,但我选择以不同的方式处理乘法任务(为了简洁)。

创建Matrix es ab后,清单 8-7 的main()方法创建Matrix并实例化ForkJoinPool。然后实例化MatMult,将这三个Matrix实例作为参数传递给MatMult(Matrix a, Matrix b, Matrix c)构造函数,并调用ForkJoinPoolT invoke(ForkJoinTask<T> task)方法开始运行这个初始任务。该方法直到初始任务及其所有子任务完成后才返回。

MatMult(Matrix a, Matrix b, Matrix c)构造函数调用MatMult(Matrix a, Matrix b, Matrix c, int row)构造函数,将-1指定为row的值。作为前面提到的invoke()方法调用的结果,compute()使用这个值来区分初始任务和子任务。

当最初调用compute()(row等于-1)时,它创建一个MatMult任务的List,并将这个List传递给RecursiveActionCollection<T> invokeAll(Collection<T> tasks)方法(继承自ForkJoinTask)。这个方法派生出所有List集合的任务,这些任务将开始执行。然后等待,直到invokeAll()方法返回(也加入到所有这些任务中),当boolean isDone()方法(也继承自ForkJoinTask)为每个任务返回true时,就会发生这种情况。

注意tasks.add(new MatMult(a, b, c, row));方法调用。这个调用为一个MatMult实例分配一个特定的row值。当invokeAll()被调用时,每个任务的compute()方法被调用并检测分配给row的不同值(除了-1)。然后,它为其特定的row执行multiplyRowByColumn(a, b, c, row);

编译清单 8-7 ( javac MatMult.java)并运行结果应用(java MatMult)。您应该观察到以下输出:

1 2 3

4 5 6

7 1

8 2

9 3

50 14

122 32

完井服务

完成服务是java.util.concurrent.CompletionService<V>接口的一个实现,它将新的异步任务的产生(生产者)与已完成任务的结果的消费(消费者)分离开来。V是任务结果的类型。

生产者通过调用其中一个submit()方法提交任务以供执行:一个方法接受可调用的参数,另一个方法接受可运行的参数以及任务完成时返回的结果。每个方法返回一个代表任务挂起完成的Future<V>实例。然后您可以调用一个poll()方法来轮询任务的完成情况,或者调用阻塞的take()方法。

消费者通过调用take()方法获得一个完成的任务。此方法会一直阻止,直到任务完成。然后它返回一个代表已完成任务的Future<V>对象。您将调用Future<V>get()方法来获得这个结果。

CompletionService<V>一起,Java 7 引入了java.util.concurrent.ExecutorCompletionService<V>类,通过提供的执行器支持任务执行。这个类确保当提交的任务完成时,它们被放在一个take()可以访问的队列中。

为了演示CompletionServiceExecutorCompletionService,我在重温我在第五章中首次提出的计算欧拉数的应用。清单 8-8 给出了一个新应用的源代码,该应用提交了两个可调用的任务来计算不同精度的数字。

Listing 8-8. Calculating Euler’s Number via a Completion Service

import java.math.BigDecimal;

import java.math.MathContext;

import java.math.RoundingMode;

import java.util.concurrent.Callable;

import java.util.concurrent.CompletionService;

import java.util.concurrent.ExecutorCompletionService;

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Future;

public class CSDemo

{

public static void main(String[] args) throws Exception

{

ExecutorService es = Executors.newFixedThreadPool(10);

CompletionService<BigDecimal> cs =

new ExecutorCompletionService<BigDecimal>(es);

cs.submit(new CalculateE(17));

cs.submit(new CalculateE(170));

Future<BigDecimal> result = cs.take();

System.out.println(result.get());

System.out.println();

result = cs.take();

System.out.println(result.get());

es.shutdown();

}

}

class CalculateE implements Callable<BigDecimal>

{

final int lastIter;

public CalculateE(int lastIter)

{

this.lastIter = lastIter;

}

@Override

public BigDecimal call()

{

MathContext mc = new MathContext(100, RoundingMode.HALF_UP);

BigDecimal result = BigDecimal.ZERO;

for (int i = 0; i <= lastIter; i++)

{

BigDecimal factorial = factorial(new BigDecimal(i));

BigDecimal res = BigDecimal.ONE.divide(factorial, mc);

result = result.add(res);

}

return result;

}

private BigDecimal factorial(BigDecimal n)

{

if (n.equals(BigDecimal.ZERO))

return BigDecimal.ONE;

else

return n.multiply(factorial(n.subtract(BigDecimal.ONE)));

}

}

清单 8-8 呈现了两个类:CSDemoCalculateECSDemo驱动应用,CalculateE描述欧拉数计算任务。

CSDemomain()方法首先创建一个将执行任务的执行器服务。然后,它创建一个完成服务来完成任务。两个计算任务随后被提交给完成服务,完成服务异步运行每个任务。对于每个任务,完成服务的take()方法被调用以返回任务的未来,其get()方法被调用以获得任务结果,然后输出。

CalculateE包含的代码与第五章中的几乎相同(见清单 5-1 )。唯一的区别是从一个LASTITER常量变成了一个lastIter变量,它记录了最后一次执行的迭代(并决定了精度的位数)。

编译清单 8-8 如下:

javac CSDemo.java

运行生成的应用,如下所示:

java CSDemo

您应该观察到以下输出:

2.718281828459045070516047795848605061178979635251032698900735004065225042504843314055887974344245741730039454062711

2.7182818284590452353602874713526624977572470936999595749669676277240766303535475945713821785251664274638961162816541248130487298653803083054255628382459134600326751445819115604942105262868564884769196304284703491677706848122126664838550045128841929851772268853216753574895628940347880297133296754744949375835005542283846314528419863840501124972044069282255484327668062074149805932978161481951711991448146506

Note

如果您想知道 executor 服务和完成服务之间的区别,请考虑一下,对于 executor 服务,在编写了提交任务的代码之后,您需要编写代码来有效地检索任务结果。有了完成服务,这项工作几乎是自动化的。看待这些结构的另一种方式是,执行器服务为任务提供一个传入队列并提供工作线程,而完成服务为任务、工作线程提供一个传入队列,并为存储任务结果提供一个输出队列。

Exercises

以下练习旨在测试您对第八章内容的理解:

Identify the two problems with thread-safe collections.   Define concurrent collection.   What is a weakly-consistent iterator?   Describe the BlockingQueue interface.   Describe the ConcurrentMap interface.   Describe the ArrayBlockingQueue and LinkedBlockingQueue BlockingQueue-implementation classes.   True or false: The concurrency-oriented collection types are part of the Collections Framework.   Describe the ConcurrentHashMap class.   Using ConcurrentHashMap, how would you check if a map contains a specific value and, when this value is absent, put this value into the map without relying on external synchronization?   Define atomic variable.   What does the AtomicIntegerArray class describe?   True or false: volatile supports atomic read-modify-write sequences.   What’s responsible for the performance gains offered by the concurrency utilities?   Describe the Fork/Join Framework.   Identify the main types that comprise the Fork/Join Framework.   To accomplish meaningful work via RecursiveAction, which one of its methods would you override?   Define completion service.   How do you use a completion service?   How do you execute tasks via a completion service?   Convert the following expressions to their atomic variable equivalents: int total = ++counter; int total = counter--;  

摘要

本章通过介绍并发集合、原子变量、Fork/Join 框架和完成服务,完成了我的并发工具之旅。

并发集合是一种存储在java.util.concurrent包中的并发高性能和高度可伸缩的面向集合的类型。它克服了线程安全集合的ConcurrentModificationException和性能问题。

原子变量是一个类的实例,它封装了单个变量,并支持对该变量进行无锁、线程安全的操作,例如AtomicInteger

Fork/Join 框架由一个特殊的执行器服务和线程池组成。executor 服务使一个任务对框架可用,并且这个任务被分解成从池中派生(由不同的线程执行)的更小的任务。任务会一直等待,直到它被加入(其子任务完成)。

完成服务是CompletionService<V>接口的一个实现,它将新的异步任务的产生(生产者)与已完成任务的结果的消费(消费者)分离开来。V是任务结果的类型。

附录 A 给出了每章练习的答案。

九、练习答案

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1700-9_​9) contains supplementary material, which is available to authorized users.

第一章到第八章的每一章都有一个“练习”部分,测试你对本章内容的理解。这些练习的答案见本附录。

第一章:线程和可运行线程

A thread is an independent path of execution through an application’s code.   A runnable is a code sequence encapsulated into an object whose class implements the Runnable interface.   The Thread class provides a consistent interface to the underlying operating system’s threading architecture. The Runnable interface supplies the code to be executed by the thread that’s associated with a Thread object.   The two ways to create a Runnable object are to instantiate an anonymous class that implements the Runnable interface and to use a lambda expression.   The two ways to connect a runnable to a Thread object are to pass the runnable to a Thread constructor that accepts a runnable argument and to subclass Thread and override its void run() method when the constructor doesn’t accept a runnable argument. Thread implements the Runnable interface, which makes Thread objects runnables as well.   The five kinds of Thread state are a name, an indication of whether the thread is alive or dead, the execution state of the thread (is it runnable?), the thread’s priority, and an indication of whether the thread is daemon or nondaemon.   The answer is false: a default thread name starts with the Thread- prefix.   You give a thread a nondefault name by calling a Thread constructor that accepts a thread name or by calling Thread’s void setName( String name) method.   You determine if a thread is alive or dead by calling Thread’s boolean isAlive() method.   The Thread.State enum’s constants are NEW (a thread that has not yet started is in this state), RUNNABLE (a thread executing in the Java virtual machine [JVM] is in this state), BLOCKED (a thread that is blocked waiting for a monitor lock is in this state), WAITING (a thread that is waiting indefinitely for another thread to perform a particular action is in this state), TIMED_WAITING (a thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state), and TERMINATED (a thread that has exited is in this state).   You obtain the current thread execution state by calling Thread’s Thread.State getState() method.   Priority is thread-relative importance.   Using setPriority() can impact an application’s portability across operating systems because different schedulers can handle a priority change in different ways.   The range of values that you can pass to Thread’s void setPriority(int priority) method are Thread.MIN_PRIORITY to Thread.MAX_PRIORITY.   The answer is true: a daemon thread dies automatically when the application’s last nondaemon thread dies so that the application can terminate.   Thread’s void start() method throws IllegalThreadStateException when called on a Thread object whose thread is running or has died.   You would stop an unending application on Windows by pressing the Ctrl and C keys simultaneously.   The methods that form Thread’s interruption mechanism are void interrupt(), static boolean interrupted(), and boolean isInterrupted().   The answer is false: the boolean isInterrupted() method doesn’t clear the interrupted status of this thread. The interrupted status is unaffected.   A thread throws InterruptedException when it’s interrupted.   A busy loop is a loop of statements designed to waste some time.   Thread’s methods that let a thread wait for another thread to die are void join(), void join(long millis), and void join(long millis, int nanos).   Thread’s methods that let a thread sleep are void sleep(long millis) and void sleep(long millis, int nanos).   Listing A-1 presents the IntSleep application that was called for in Chapter 1.   Listing A-1. Interrupting a Sleeping Background Thread

public class IntSleep

{

public static void main(String[] args)

{

Runnable r = new Runnable()

{

@Override

public void run()

{

while (true)

{

System.out.println("hello");

try

{

Thread.sleep(100);

}

catch (InterruptedException ie)

{

System.out.println("interrupted");

break;

}

}

}

};

Thread t = new Thread(r);

t.start();

try

{

Thread.sleep(2000);

}

catch (InterruptedException ie)

{

}

t.interrupt();

}

}

第二章:同步

The three problems with threads are race conditions, data races, and cached variables.   The answer is false: when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the scheduler, you have a race condition.   Synchronization is a JVM feature that ensures that two or more concurrent threads don’t simultaneously execute a critical section.   The two properties of synchronization are mutual exclusion and visibility.   Synchronization is implemented in terms of monitors, which are concurrency constructs for controlling access to critical sections, which must execute indivisibly. Each Java object is associated with a monitor, which a thread can lock or unlock acquiring and releasing the monitor’s lock token.   The answer is true: a thread that has acquired a lock doesn’t release this lock when it calls one of Thread’s sleep() methods.   You specify a synchronized method by including the keyword synchronized in the method header.   You specify a synchronized block by specifying the syntax synchronized(object) {}.   Liveness refers to something beneficial happening eventually.   The three liveness challenges are deadlock, livelock, and starvation (also known as indefinite postponement).   The volatile keyword differs from synchronized in that volatile deals with visibility only, whereas synchronized deals with mutual exclusion and visibility.   The answer is true: Java also lets you safely access a final field without the need for synchronization.   The thread problems with the CheckingAccount class are the check-then-act race condition in the withdraw() method between if (amount <= balance) and balance -= amount; (which results in more money being withdrawn than is available for withdrawal) and the potentially cached balance field. The balance field can be cached on multiprocessor/multicore systems and the cached copy used by the withdrawal thread might not contain the initial balance set in the constructor by the default main thread.   Listing A-2 presents the CheckingAccount application that was called for in Chapter 2.   Listing A-2. Fixing a Problematic Checking Account

public class CheckingAccount

{

private volatile int balance;

public CheckingAccount(int initialBalance)

{

balance = initialBalance;

}

public synchronized boolean withdraw(int amount)

{

if (amount <= balance)

{

try

{

Thread.sleep((int) (Math.random() * 200));

}

catch (InterruptedException ie)

{

}

balance -= amount;

return true;

}

return false;

}

public static void main(String[] args)

{

final CheckingAccount ca = new CheckingAccount(100);

Runnable r = new Runnable()

{

@Override

public void run()

{

String name = Thread.currentThread().getName();

for (int i = 0; i < 10; i++)

System.out.println (name + " withdraws $10: " +

ca.withdraw(10));

}

};

Thread thdHusband = new Thread(r);

thdHusband.setName("Husband");

Thread thdWife = new Thread(r);

thdWife.setName("Wife");

thdHusband.start();

thdWife.start();

}

}

这个应用使用volatile来处理潜在的缓存问题,使用synchronized来处理互斥需求。

第三章:等待和通知

A condition is a prerequisite for continued execution.   The API that supports conditions consists of Object’s three wait() methods, one notify() method, and one notifyAll() method. The wait() methods wait for a condition to exist; the notify() and notifyAll() methods notify the waiting thread when the condition exists.   The answer is true: the wait() methods are interruptible.   You would call the notifyAll() method to wake up all threads that are waiting on an object’s monitor.   The answer is false: a thread that has acquired a lock releases this lock when it calls one of Object’s wait() methods.   A condition queue is a data structure that stores threads waiting for a condition to exist. The waiting threads are known as the wait set.   When you call any of the API’s methods outside of a synchronized context, IllegalMonitorStateException is thrown.   A spurious wakeup is a thread waking up without being notified, interrupted, or timing out.   You should call a wait() method in a loop context to ensure liveness and safety.   Listing A-3 presents the Await application that was called for in Chapter 3.   Listing A-3. Using wait() and notifyAll() to Create a Higher-Level Concurrency Construct

public class Await

{

static volatile int count;

public static void main(String[] args)

{

Runnable r = () ->

{

Thread curThread = Thread.currentThread();

System.out.printf("%s has entered runnable and is " +

"waiting%n", curThread.getName());

synchronized(Await.class)

{

count++;

try

{

Thread.sleep(2000);

while (count < 3)

Await.class.wait();

}

catch (InterruptedException ie)

{

}

}

System.out.printf("%s has woken up and is " +

"terminating%n",

curThread.getName());

};

Thread thdA = new Thread(r, "thdA");

Thread thdB = new Thread(r, "thdB");

Thread thdC = new Thread(r, "thdC");

thdA.start();

thdB.start();

thdC.start();

r = new Runnable()

{

@Override

public void run()

{

try

{

while (count < 3)

Thread.sleep(100);

synchronized(Await.class)

{

Await.class.notifyAll();

}

}

catch (InterruptedException ie)

{

}

}

};

Thread thd = new Thread(r);

thd.start();

}

}

第四章:附加线程能力

A thread group is a set of threads. It’s represented by the ThreadGroup class.   You might use a thread group to perform a common operation on its threads, to simplify thread management.   You should avoid using thread groups because the most useful ThreadGroup methods have been deprecated and because of the “time of check to time of use” race condition between obtaining a count of active threads and enumerating those threads.   You should be aware of thread groups because of ThreadGroup’s contribution in handling exceptions that are thrown while a thread is executing.   A thread-local variable is a variable that provides a separate storage slot to each thread that accesses the variable. It’s represented by the ThreadLocal class.   The answer is true: if an entry doesn’t exist in the calling thread’s storage slot when the thread calls get(), this method calls initialValue().   You would pass a value from a parent thread to a child thread by working with the InheritableThreadLocal class.   The classes that form the Timer Framework are Timer and TimerTask.   The answer is false: Timer() creates a new timer whose task-execution thread runs as a nondaemon thread.   In fixed-delay execution, each execution is scheduled relative to the actual execution time of the previous execution. When an execution is delayed for any reason (such as garbage collection), subsequent executions are also delayed.   You call the schedule() methods to schedule a task for fixed-delay execution.   In fixed-rate execution, each execution is scheduled relative to the scheduled execution time of the initial execution. When an execution is delayed for any reason (such as garbage collection), two or more executions will occur in rapid succession to “catch up.”   The difference between Timer’s cancel() method and TimerTask’s cancel() method is as follows: Timer’s cancel() method terminates the timer, discarding any currently scheduled timer tasks. In contrast, TimerTask’s cancel() method cancels the invoking timer task only.   Listing A-4 presents the BackAndForth application that was called for in Chapter 4.   Listing A-4. Repeatedly Moving an Asterisk Back and Forth via a Timer

import java.util.Timer;

import java.util.TimerTask;

public class BackAndForth

{

static enum Direction { FORWARDS, BACKWARDS }

public static void main(String[] args)

{

TimerTask task = new TimerTask()

{

final static int MAXSTEPS = 20;

volatile Direction direction = Direction.FORWARDS;

volatile int steps = 0;

@Override

public void run()

{

switch (direction)

{

case FORWARDS : System.out.print("\b ");

System.out.print("*");

break;

case BACKWARDS: System.out.print("\b ");

System.out.print("\b\b*");

}

if (++steps == MAXSTEPS)

{

direction =

(direction == Direction.FORWARDS)

? Direction.BACKWARDS

: Direction.FORWARDS;

steps = 0;

}

}

};

Timer timer = new Timer();

timer.schedule(task, 0, 100);

}

}

第五章:并发工具和执行器

The concurrency utilities are a framework of classes and interfaces that overcome problems with Java’s low-level thread capabilities. Specifically, low-level concurrency primitives such as synchronized and wait()/notify() are often hard to use correctly, too much reliance on the synchronized primitive can lead to performance issues, which affect an application’s scalability, and higher-level constructs such as thread pools and semaphores aren’t included with Java’s low-level thread capabilities.   The packages in which the concurrency utilities types are stored are java.util.concurrent, java.util.concurrent.atomic, and java.util.concurrent.locks.   A task is an object whose class implements the Runnable interface (a runnable task) or the Callable interface (a callable task).   An executor is an object whose class directly or indirectly implements the Executor interface, which decouples task submission from task-execution mechanics.   The Executor interface focuses exclusively on Runnable, which means that there’s no convenient way for a runnable task to return a value to its caller (because Runnable’s run() method doesn’t return a value); Executor doesn’t provide a way to track the progress of executing runnable tasks, cancel an executing runnable task, or determine when the runnable task finishes execution; Executor cannot execute a collection of runnable tasks; and Executor doesn’t provide a way for an application to shut down an executor (much less to properly shut down an executor).   Executor’s limitations are overcome by providing the ExecutorService interface.   The differences existing between Runnable’s run() method and Callable’s call() method are as follows: run() cannot return a value, whereas call() can return a value; and run() cannot throw checked exceptions, whereas call() can throw checked exceptions.   The answer is false: you can throw checked and unchecked exceptions from Callable’s call() method but can only throw unchecked exceptions from Runnable’s run() method.   A future is an object whose class implements the Future interface. It represents an asynchronous computation and provides methods for canceling a task, for returning a task’s value, and for determining whether or not the task has finished.   The Executors class’s newFixedThreadPool() method creates a thread pool that reuses a fixed number of threads operating off of a shared unbounded queue. At most, nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue for an available thread. If any thread terminates because of a failure during execution before the executor shuts down, a new thread will take its place when needed to execute subsequent tasks. The threads in the pool will exist until the executor is explicitly shut down.   Listing A-5 presents the CountingThreads application that was called for in Chapter 5.   Listing A-5. Executor-Based Counting Threads

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

public class CountingThreads

{

public static void main(String[] args)

{

Runnable r = new Runnable()

{

@Override

public void run()

{

String name = Thread.currentThread().getName();

int count = 0;

while (true)

System.out.println(name + ": " + count++);

}

};

ExecutorService es = Executors.newFixedThreadPool(2);

es.submit(r);

es.submit(r);

}

}

Listing A-6 presents the CountingThreads application with custom-named threads that was called for in Chapter 5.   Listing A-6. Executor-Based Counting Threads A and B

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.ThreadFactory;

public class CountingThreads

{

public static void main(String[] args)

{

Runnable r = new Runnable()

{

@Override

public void run()

{

String name = Thread.currentThread().getName();

int count = 0;

while (true)

System.out.println(name + ": " + count++);

}

};

ExecutorService es =

Executors.newSingleThreadExecutor(new NamedThread("A"));

es.submit(r);

es = Executors.newSingleThreadExecutor(new NamedThread("B"));

es.submit(r);

}

}

class NamedThread implements ThreadFactory

{

private volatile String name; // newThread() could be called by a

// different thread

NamedThread(String name)

{

this.name = name;

}

@Override

public Thread newThread(Runnable r)

{

return new Thread(r, name);

}

}

第六章:同步器

A synchronizer is a class that facilitates a common form of synchronization.   A countdown latch causes one or more threads to wait at a “gate” until another thread opens this gate, at which point these other threads can continue. It consists of a count and operations for “causing a thread to wait until the count reaches zero” and “decrementing the count”.   When CountDownLatch’s void countDown() method is called and the count reaches zero, all waiting threads are released.   A cyclic barrier lets a set of threads wait for each other to reach a common barrier point. The barrier is cyclic because it can be reused after the waiting threads are released. This synchronizer is useful in applications involving a fixed-size party of threads that must occasionally wait for each other.   The answer is false: CyclicBarrier’s int await() method throws BrokenBarrierException when the barrier is reset while any thread is waiting or when the barrier is broken when await() is invoked.   An exchanger provides a synchronization point where threads can swap objects. Each thread presents some object on entry to the exchanger’s exchange() method, matches with a partner thread, and receives its partner’s object on return.   Exchanger’s V exchange(V x) method waits for another thread to arrive at this exchange point (unless the calling thread is interrupted), and then transfers the given object to it, receiving the other thread’s object in return.   A semaphore maintains a set of permits for restricting the number of threads that can access a limited resource. A thread attempting to acquire a permit when no permits are available blocks until some other thread releases a permit.   The two kinds of semaphores are counting semaphores (the current values can be incremented past 1) and binary semaphores or mutexs (the current values can be only 0 or 1).   A phaser is a more flexible cyclic barrier. Like a cyclic barrier, a phaser lets a group of threads wait on a barrier; these threads continue after the last thread arrives. A phaser also offers the equivalent of a barrier action. Unlike a cyclic barrier, which coordinates a fixed number of threads, a phaser can coordinate a variable number of threads, which can register at any time. To implement this capability, a phaser uses phases (current states) and phase numbers (current state identifiers).   Phaser’s int register() method returns the phase number to classify the arrival. If this value is negative, this phaser has terminated, in which case registration has no effect. This number is known as the arrival phase number.   Listing A-7 presents the PC application that was called for in Chapter 6.   Listing A-7. Semaphore-Based Producer and Consumer

import java.util.concurrent.Semaphore;

public class PC

{

public static void main(String[] args)

{

Shared s = new Shared();

Semaphore semCon = new Semaphore(0);

Semaphore semPro = new Semaphore(1);

new Producer(s, semPro, semCon).start();

new Consumer(s, semPro, semCon).start();

}

}

class Shared

{

private char c;

void setSharedChar(char c)

{

this.c = c;

}

char getSharedChar()

{

return c;

}

}

class Producer extends Thread

{

private final Shared s;

private final Semaphore semPro, semCon;

Producer(Shared s, Semaphore semPro, Semaphore semCon)

{

this.s = s;

this.semPro = semPro;

this.semCon = semCon;

}

@Override

public void run()

{

for (char ch = 'A'; ch <= 'Z'; ch++)

{

try

{

semPro.acquire();

}

catch (InterruptedException ie)

{

}

s.setSharedChar(ch);

System.out.println(ch + " produced by producer.");

semCon.release();

}

}

}

class Consumer extends Thread

{

private final Shared s;

private final Semaphore semPro, semCon;

Consumer(Shared s, Semaphore semPro, Semaphore semCon)

{

this.s = s;

this.semPro = semPro;

this.semCon = semCon;

}

@Override

public void run()

{

char ch;

do

{

try

{

semCon.acquire();

}

catch (InterruptedException ie)

{

}

ch = s.getSharedChar();

System.out.println(ch + " consumed by consumer.");

semPro.release();

}

while (ch != 'Z');

}

}

第七章:锁紧框架

A lock is an instance of a class that implements the Lock interface, which provides more extensive locking operations than can be achieved via the synchronized reserved word. Lock also supports a wait/notification mechanism through associated Condition objects.   The biggest advantage that Lock objects hold over the intrinsic locks that are obtained when threads enter critical sections (controlled via the synchronized reserved word) is their ability to back out of an attempt to acquire a lock.   The answer is true: ReentrantLock’s unlock() method throws IllegalMonitorStateException when the calling thread doesn’t hold the lock.   You obtain a Condition instance for use with a particular Lock instance by invoking Lock’s Condition newCondition() method.   The answer is false: ReentrantReadWriteLock() creates an instance of ReentrantReadWriteLock without a fair ordering policy.   Introduced by JDK 8, StampedLock is a capability-based lock with three modes for controlling read/write access. It differentiates between exclusive and nonexclusive locks in a manner that’s similar to ReentrantReadWriteLock, but also allows for optimistic reads, which ReentrantReadWriteLock doesn’t support.   The purpose of LockSupport is to provide basic thread-blocking primitives for creating locks and other synchronization classes.   Listing A-8 presents the ID class that was called for in Chapter 7.   Listing A-8. ReentrantLock-Based ID Generator

import java.util.concurrent.locks.ReentrantLock;

public class ID

{

private static int counter; // initialized to 0 by default

private final static ReentrantLock lock = new ReentrantLock();

public static int getID()

{

lock.lock();

try

{

int temp = counter + 1;

try

{

Thread.sleep(1);

}

catch (InterruptedException ie)

{

}

return counter = temp;

}

finally

{

lock.unlock();

}

}

}

第八章:额外的并发工具

The two problems with thread-safe collections are the possibility of thrown ConcurrentModificationException objects and poor performance. It’s necessary to acquire a lock before iterating over a collection that might be modified by another thread during the iteration. If a lock isn’t acquired and the collection is modified, it’s highly likely that ConcurrentModificationException will be thrown. Also, performance suffers when synchronized collections are accessed frequently from multiple threads.   A concurrent collection is a concurrency performant and highly-scalable collection-oriented type that is stored in the java.util.concurrent package.   A weakly-consistent iterator is an iterator with the following properties:

  • 迭代开始后移除但尚未通过迭代器的next()方法返回的元素不会被返回。
  • 迭代开始后添加的元素可能会返回,也可能不会返回。
  • 在集合的迭代过程中,任何元素都不会返回多次,无论在迭代过程中对集合做了什么更改。

  BlockingQueue is a subinterface of java.util.Queue that also supports blocking operations that wait for the queue to become nonempty before retrieving an element and wait for space to become available in the queue before storing an element.   ConcurrentMap is a subinterface of java.util.Map that declares additional indivisible putIfAbsent(), remove(), and replace() methods.   ArrayBlockingQueue is a bounded blocking queue backed by an array. LinkedBlockingQueue is an optionally-bounded blocking queue based on linked nodes.   The answer is true: the concurrency-oriented collection types are part of the Collections Framework.   ConcurrentHashMap behaves like HashMap but has been designed to work in multithreaded contexts without the need for explicit synchronization.   Using ConcurrentHashMap, you would call its putIfAbsent() method to check if a map contains a specific value and, when this value is absent, put this value into the map without relying on external synchronization.   An atomic variable is an instance of a class that encapsulates a single variable and supports lock-free, thread-safe operations on that variable, for example, AtomicInteger.   The AtomicIntegerArray class describes an int array whose elements may be updated atomically.   The answer is false: volatile doesn’t support atomic read-modify-write sequences.   The compare-and-swap instruction is responsible for the performance gains offered by the concurrency utilities.   The Fork/Join Framework consists of a special executor service and thread pool. The executor service makes a task available to the framework, and this task is broken down into smaller tasks that are forked (executed by different threads) from the pool. A task waits until it’s joined (its subtasks finish).   The main types that comprise the Fork/Join Framework are the java.util.concurrent package’s ForkJoinPool, ForkJoinTask, ForkJoinWorkerThread, RecursiveAction, RecursiveTask, and CountedCompleter classes.   To accomplish meaningful work via RecursiveAction, you would override its void compute() method.   A completion service is an implementation of the CompletionService<V> interface that decouples the production of new asynchronous tasks (a producer) from the consumption of the results of completed tasks (a consumer). V is the type of a task result.   You use a completion service as follows: Submit a task for execution (via a worker thread) by calling one of CompletionService<V>’s submit() methods. Each method returns a Future<V> instance that represents the pending completion of the task. You can then call a poll() method to poll for the task’s completion or call the blocking take() method. A consumer takes a completed task by calling the take() method. This method blocks until a task has completed. It then returns a Future<V> object that represents the completed task. You would call Future<V>’s get() method to obtain this result.   You execute tasks via a completion service by working with the ExecutorCompletionService<V> class, which implements CompletionService<V>, and which supports task execution via a provided executor.   The atomic variable equivalent of int total = ++counter; is as follows: AtomicInteger counter = new AtomicInteger(0); int total = counter.incrementAndGet(); The atomic variable equivalent of int total = counter--; is as follows: AtomicInteger counter = new AtomicInteger(0); int total = counter.getAndDecrement();

十、Swing 线程

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1700-9_​10) contains supplementary material, which is available to authorized users.

Swing 是一个独立于平台的、基于模型-视图-控制器GUI 工具包,用于创建 Java 应用的图形前端。在本附录中,我首先探索 Swing 的线程架构,然后探索 Swing APIs,以避免在图形环境中使用额外线程时出现问题。最后,我将展示一个基于 Swing 的幻灯片放映应用,作为本附录内容的一个重要示例,并作为本书的一个有趣结尾。

Note

我假设您对 Swing APIs 以及 Swing 应用的架构有一些经验。

单线程编程模型

Swing 遵循一个单线程的 ?? 编程模型。它被设计成单线程而不是多线程的,因为多线程图形工具包的设计经验表明,它们不可避免地会导致死锁和竞争情况。要了解关于这些问题的更多信息,请查看“为什么 GUI 是单线程的?”博文( http://codeidol.com/java/java-concurrency/GUI-Applications/Why-are-GUIs-Single-threaded/ )。

用于渲染图形和处理事件的线程被称为事件调度线程(EDT)。EDT 处理来自底层抽象窗口工具包的事件队列的事件,并调用 GUI 组件(如按钮)事件侦听器,后者处理该线程上的事件。组件甚至在 EDT 上重绘自己(响应导致paintComponent()paintBorder()paintChildren()方法调用的paint()方法调用)。

注意代码如何与 EDT 交互,以确保 Swing 应用正常工作。有两条规则需要记住:

  • 总是在东部时间创建 Swing GUIs。
  • 不要延迟美国东部时间。

Swing 是单线程的一个结果是,您必须只在 EDT 上创建 Swing 应用的 GUI。在任何其他线程上创建这个 GUI 都是不正确的,包括运行 Java 应用的main()方法的默认主线程。

大多数 Swing 对象(比如javax.swing.JFrame对象,它用菜单栏和边框描述 GUI 顶层“框架”窗口)都不是线程安全的。从多个线程访问这些对象存在线程干扰和/或内存不一致错误的风险:

  • 线程干扰:两个线程在处理相同数据时执行两种不同的操作。例如,一个线程读取一个长整数计数器变量,而另一个线程更新这个变量。因为在 32 位机器上读取或写入一个长整数需要两次读/写访问,所以有可能读取线程读取该变量当前值的一部分,然后写入线程更新该变量,然后读取线程读取该变量的其余部分。结果是读取线程具有不正确的值。
  • 内存不一致错误:在不同处理器或处理器内核上运行的两个或多个线程对相同数据的视图不一致。例如,一个处理器或内核上的写线程更新一个counter变量,然后另一个处理器或内核上的读线程读取这个变量。但是,因为使用了缓存机制来提高性能,所以两个线程都不会访问主内存中变量的单个副本。相反,每个线程从本地内存(缓存)中访问自己的变量副本。

当 GUI 不是在 EDT 上创建时,这些问题是如何发生的?约翰·祖科夫斯基在他题为“ Swing 线程和事件调度线程”(www.javaworld.com/article/2077754/core-java/swing-threading-and-the-event-dispatch-thread.html)的 JavaWorld 文章中演示了一个场景。

祖科夫斯基展示了一个向框架窗口容器组件添加容器监听器的例子。在框架中添加或移除组件时,将调用侦听器方法。他演示了在默认主线程上实现框架窗口之前,在监听器方法中运行 EDT 代码。

Note

实现意味着组件的paint()方法已经被调用或者可能被调用。通过在这个容器组件上调用setVisible(true)show()pack()中的一个来实现框架窗口。框架窗口实现后,它包含的所有组件也实现了。实现组件的另一种方式是将它添加到已经实现的容器中。

在 EDT 开始在侦听器方法中运行之后,并且在默认主线程继续初始化 GUI 的同时,组件可以由默认主线程创建并由 EDT 访问。EDT 可能试图在这些组件存在之前访问它们;这样做可能会导致应用崩溃。

即使默认主线程在 EDT 从 listener 方法访问组件之前创建了这些组件,EDT 也可能会有不一致的视图(因为缓存),并且无法访问对新组件的引用。应用崩溃(可能是抛出的java.lang.NullPointerException对象)很可能会发生。

清单 B-1 向ViewPage展示源代码,这是一个用于查看网页 HTML 的 Swing 应用。这种应用存在两个问题。

Listing B-1. A Problematic Web Page HTMLViewer Swing Application

import java.awt.BorderLayout;

import java.awt.Dimension;

import java.awt.EventQueue;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.io.InputStream;

import java.io.IOException;

import java.net.URL;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.JTextField;

public class ViewPage

{

public static void main(String[] args)

{

final JFrame frame = new JFrame("View Page");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JPanel panel = new JPanel();

panel.add(new JLabel("Enter URL"));

final JTextField txtURL = new JTextField(40);

panel.add(txtURL);

frame.getContentPane().add(panel, BorderLayout.NORTH);

final JTextArea txtHTML = new JTextArea(10, 40);

frame.getContentPane().add(new JScrollPane (txtHTML),

BorderLayout.CENTER);

ActionListener al = (ae) ->

{

InputStream is = null;

try

{

URL url = new URL(txtURL.getText());

is = url.openStream();

StringBuilder sb = new StringBuilder();

int b;

while ((b = is.read()) != -1)

sb.append((char) b);

txtHTML.setText(sb.toString());

}

catch (IOException ioe)

{

txtHTML.setText(ioe.getMessage());

}

finally

{

txtHTML.setCaretPosition(0);

if (is != null)

try

{

is.close();

}

catch (IOException ioe)

{

}

}

};

txtURL.addActionListener(al);

frame.pack();

frame.setVisible(true);

}

}

清单 B-1 的main()方法创建了一个 GUI,由一个用于输入网页 URL 的文本字段和一个用于显示页面 HTML 的可滚动文本区域组成。输入 URL 后按 Enter 键会导致ViewPage获取并显示 HTML。

编译清单 B-1 如下:

javac ViewPage.java

运行生成的应用,如下所示:

java ViewPage

您应该看到如图 B-1 所示的 GUI(填充了一个示例 URL 和部分结果网页的 HTML)。

A978-1-4842-1700-9_10_Fig1_HTML.jpg

图 B-1。

Entering a URL in a text field and viewing web page output in a scrollable text area

这个应用的第一个问题是 GUI 是在默认的主线程上创建的,而不是在 EDT 上。虽然在运行ViewPage时可能不会遇到问题,但是存在潜在的线程干扰和内存不一致问题。

这个应用的第二个问题是,EDT 运行动作监听器来响应文本字段上的 Enter 键,它被打开 URL 的输入流并将其内容读入字符串生成器的代码所延迟。在此期间,GUI 没有响应。

线程 API

Swing 提供的 API 克服了 EDT 的上述问题。在本节中,我将向您介绍这些 API。我还将向您介绍 Swing 版本的计时器,它与我在第四章中介绍的计时器框架有很大不同。

SwingUtilities 和 EventQueue

javax.swing. SwingUtilities类提供了一组在 Swing 上下文中有用的static方法。其中三种方法对于使用 EDT 和避免前面的问题特别有用:

  • void invokeAndWait(Runnable doRun):使doRun.run()在 EDT 上同步执行。这个调用会一直阻塞,直到所有未决事件都被处理完,然后doRun.run()返回。在等待 EDT 完成执行doRun.run()的过程中,该方法中断时invokeAndWait()抛出java.lang.InterruptedException。当应用线程需要从除 EDT 之外的任何线程更新 GUI 时,应该使用从doRun.run(). invokeAndWait()抛出异常时抛出java.lang.reflect.InvocationTargetException。它不应该从美国东部时间调用。
  • void invokeLater( Runnable doRun):导致doRun.run()在 EDT 上异步执行。这发生在所有未决事件处理完毕之后。当应用线程需要更新 GUI 时,应该使用invokeLater()。可以从任何线程调用它。
  • boolean isEventDispatchThread():当调用线程为 EDT 时,返回true;否则,返回false

invokeAndWait()invokeLater()isEventDispatchThread()方法是调用java.awt.EventQueue类中等效方法的包装器。虽然您可以用SwingUtilities作为这些方法的前缀,但我使用EventQueue作为前缀(出于习惯)。

通常使用invokeLater()根据以下模式构建 Swing GUI:

Runnable r = ... // ... refers to the runnable’s anonymous class or lambda

EventQueue.invokeLater(r);

清单 B-2 给出了第二个版本ViewPage的源代码,该版本使用invokeLater()在 EDT 上构建 Swing GUI。

Listing B-2. Constructing the HTML Viewer Swing Application GUI on the EDT

import java.awt.BorderLayout;

import java.awt.Dimension;

import java.awt.EventQueue;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.io.InputStream;

import java.io.IOException;

import java.net.URL;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.JTextField;

public class ViewPage

{

public static void main(String[] args)

{

Runnable r = () ->

{

final JFrame frame = new JFrame("View Page");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JPanel panel = new JPanel();

panel.add(new JLabel("Enter URL"));

final JTextField txtURL = new JTextField(40);

panel.add(txtURL);

frame.getContentPane().add(panel, BorderLayout.NORTH);

final JTextArea txtHTML = new JTextArea(10, 40);

frame.getContentPane().add(new JScrollPane (txtHTML),

BorderLayout.CENTER);

ActionListener al = (ae) ->

{

InputStream is = null;

try

{

URL url = new URL(txtURL.getText());

is = url.openStream();

StringBuilder sb = new StringBuilder();

int b;

while ((b = is.read()) != -1)

sb.append((char) b);

txtHTML.setText(sb.toString());

}

catch (IOException ioe)

{

txtHTML.setText(ioe.getMessage());

}

finally

{

txtHTML.setCaretPosition(0);

if (is != null)

try

{

is.close();

}

catch (IOException ioe)

{

}

}

};

txtURL.addActionListener(al);

frame.pack();

frame.setVisible(true);

};

EventQueue.invokeLater(r) ;

}

}

列表 B-2 解决了一个问题,但是我们仍然必须防止东部夏令时被延迟。我们可以通过创建一个工作线程来读取页面,并使用invokeAndWait()在 EDT 上用页面内容更新可滚动文本区域来解决这个问题。查看清单 B-3 。

Listing B-3. Constructing the HTML Viewer Swing Application GUI on a Non-Delayed EDT

import java.awt.BorderLayout;

import java.awt.Dimension;

import java.awt.EventQueue;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.io.InputStream;

import java.io.IOException;

import java.lang.reflect.InvocationTargetException;

import java.net.URL;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.JTextField;

public class ViewPage

{

public static void main(String[] args)

{

Runnable r = () ->

{

final JFrame frame = new JFrame("View Page");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JPanel panel = new JPanel();

panel.add(new JLabel("Enter URL"));

final JTextField txtURL = new JTextField(40);

panel.add(txtURL);

frame.getContentPane().add(panel, BorderLayout.NORTH);

final JTextArea txtHTML = new JTextArea(10, 40);

frame.getContentPane().add(new JScrollPane (txtHTML),

BorderLayout.CENTER);

ActionListener al = (ae) ->

{

txtURL.setEnabled(false);

Runnable worker = () ->

{

InputStream is = null;

try

{

URL url = new URL(txtURL.getText());

is = url.openStream();

final StringBuilder sb = new StringBuilder();

int b;

while ((b = is.read()) != -1)

sb.append((char) b);

Runnable r1 = () ->

{

txtHTML.setText(sb.toString());

txtURL.setEnabled(true);

};

try

{

EventQueue.invokeAndWait(r1) ;

}

catch (InterruptedException ie)

{

}

catch (InvocationTargetException ite)

{

}

}

catch (final IOException ioe)

{

Runnable r1 = () ->

{

txtHTML.setText(ioe.getMessage());

txtURL.setEnabled(true);

};

try

{

EventQueue.invokeAndWait(r1) ;

}

catch (InterruptedException ie)

{

}

catch (InvocationTargetException ite)

{

}

}

finally

{

Runnable r1 = () ->

{

txtHTML.setCaretPosition(0);

txtURL.setEnabled(true);

};

try

{

EventQueue.invokeAndWait(r1) ;

}

catch (InterruptedException ie)

{

}

catch (InvocationTargetException ite)

{

}

if (is != null)

try

{

is.close();

}

catch (IOException ioe)

{

}

}

};

new Thread(worker).start();

};

txtURL.addActionListener(al);

frame.pack();

frame.setVisible(true);

};

EventQueue.invokeLater(r);

}

}

我选择了在获取页面时禁用文本字段以进一步输入,并在之后启用它。您仍然可以随时关闭 GUI。

虽然清单 B-3 解决了 GUI 无响应的问题,但是这个解决方案有些冗长。幸运的是,有一个替代的解决方案。

摇摆工人

Swing 提供了javax.swing. SwingWorker类来适应长时间运行的任务(比如读取 URL 内容),减少了冗长性。您必须继承这个abstract类,并覆盖一个或多个方法来完成有用的工作。

SwingWorker的通用类型是SwingWorker<T, V>。参数TV分别标识最终和中间任务结果类型。

您重写了protected abstract T doInBackground()方法,在一个工作线程上执行一个长时间运行的任务,并返回一个类型为T的结果(没有结果时,Void是返回类型)。当这个方法完成时,在 EDT 上调用protected void done()方法。默认情况下,此方法不执行任何操作。但是,您可以覆盖done()来安全地更新 GUI。

当任务运行时,您可以通过调用protected void publish(V... chunks)方法定期向 EDT 发布结果。这些结果由覆盖的protected void process(List<V> chunks)方法检索,该方法的代码在 EDT 上运行。如果没有要处理的中间结果,可以为V指定Void(避免使用publish()process()方法)。

SwingWorker提供了另外两种你需要了解的方法。首先,void execute()调度调用的SwingWorker对象在一个工作线程上执行。第二, T get()等待doInBackground()完成,然后返回最终结果。

Note

当试图检索从doInBackground()返回的对象时发生异常,则SwingWorkerget()方法抛出一个java.util.concurrent.ExecutionException类的实例。这也能扔了InterruptedException

清单 B-4 将源代码呈现给最终的ViewPage应用,该应用使用SwingWorker而不是invokeAndWait()

Listing B-4. Constructing the HTML Viewer Swing Application GUI on a Non-Delayed EDT, Revisited

import java.awt.BorderLayout;

import java.awt.Dimension;

import java.awt.EventQueue;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.io.InputStream;

import java.io.IOException;

import java.net.URL;

import java.util.concurrent.ExecutionException;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.JScrollPane;

import javax.swing.JTextArea;

import javax.swing.JTextField;

import javax.swing.SwingWorker;

public class ViewPage

{

public static void main(String[] args)

{

Runnable r = () ->

{

final JFrame frame = new JFrame("View Page");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JPanel panel = new JPanel();

panel.add(new JLabel("Enter URL"));

final JTextField txtURL = new JTextField(40);

panel.add(txtURL);

frame.getContentPane().add(panel, BorderLayout.NORTH);

final JTextArea txtHTML = new JTextArea(10, 40);

frame.getContentPane().add(new JScrollPane (txtHTML),

BorderLayout.CENTER);

ActionListener al = (ae) ->

{

txtURL.setEnabled(false);

class GetHTML extends SwingWorker<StringBuilder, Void>

{

private final String url;

GetHTML(String url)

{

this.url = url;

}

@Override

public StringBuilder doInBackground()

{

StringBuilder sb = new StringBuilder();

InputStream is = null;

try

{

URL url = new URL(this.url);

is = url.openStream();

int b;

while ((b = is.read()) != -1)

sb.append((char) b);

return sb;

}

catch (IOException ioe)

{

sb.setLength(0);

sb.append(ioe.getMessage());

return sb;

}

finally

{

if (is != null)

try

{

is.close();

}

catch (IOException ioe)

{

}

}

}

@Override

public void done()

{

try

{

StringBuilder sb = get();

txtHTML.setText(sb.toString());

txtHTML.setCaretPosition(0);

}

catch (ExecutionException ee)

{

txtHTML.setText(ee.getMessage());

}

catch (InterruptedException ie)

{

txtHTML.setText("Interrupted");

}

txtURL.setEnabled(true);

}

}

new GetHTML(txtURL.getText()).execute();

};

txtURL.addActionListener(al);

frame.pack();

frame.setVisible(true);

};

EventQueue.invokeLater(r);

}

}

这个ViewPage的最终版本依赖于GetHTML,一个在动作监听器 lambda 主体中声明的本地SwingWorker子类,在一个工作线程上读取网页(保持用户界面响应),并在 EDT 上用 HTML 更新用户界面(Swing 代码必须在 EDT 上执行)。

当 lambda 运行时(用户在文本字段中输入 URL 后按 Enter),它用文本字段的文本实例化GetHTML(文本字段不能从工作线程访问,因为 Swing 是单线程的)并调用SwingWorkerexecute()方法。

execute()导致GetHTML的覆盖doInBackground()方法在一个工作线程上被调用,该线程用 HTML/错误文本填充一个java.lang.StringBuilder对象并返回该对象。然后 EDT 调用覆盖的done()方法,该方法通过调用SwingWorkerget()方法来访问StringBuilder对象,并用这些内容填充文本区域。

计时器

Swing 提供了javax.swing.Timer类(作为定时器框架的简化版本——参见第四章)来定期在 EDT 上执行 Swing 代码。在最初的延迟之后,它向注册的侦听器触发一个操作事件,此后,通过事件之间的延迟重复触发事件。

调用Timer(int delay,ActionListenerlistener)构造函数创建一个计时器,以初始和事件间delay为单位(以毫秒为单位),以初始动作listener(可能是null)作为每delay毫秒发送的事件的目标。

delay参数值用作初始延迟和事件间延迟。您也可以通过调用void setInitialDelay(int initialDelay)void setDelay(int delay)方法来分别设置这些值。

Note

用一个false参数调用Timervoid setRepeats(boolean flag)方法,指示计时器只发送一个动作事件。

调用void addActionListener( ActionListener listener)添加另一个动作listener,调用void removeActionListener(ActionListener listener)删除之前注册的动作listener。调用 ActionListener [] getActionListeners()获取所有注册的监听器。

新创建的计时器处于停止状态。要启动计时器,调用它的void start()方法。相反,您可以调用void stop()来终止计时器。您可能还想调用boolean isRunning()来确定计时器是否正在运行。

清单 B-5 将源代码呈现给一个Counter应用,该应用创建一个计时器,通过标签持续显示运行计数。

Listing B-5. Starting and Stopping a Count

import java.awt.EventQueue;

import java.awt.FlowLayout;

import java.awt.event.ActionListener;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.Timer;

public class Counter extends JFrame

{

int count;

public Counter(String title)

{

super(title);

setDefaultCloseOperation(EXIT_ON_CLOSE);

JPanel pnl = new JPanel();

((FlowLayout) pnl.getLayout()).setHgap(20);

final JLabel lblCount = new JLabel("");

pnl.add(lblCount);

final JButton btnStartStop = new JButton("Start");

ActionListener al = (ae) ->

{

++count;

lblCount.setText(count + " ");

};

final Timer timer = new Timer(30, al);

al = (ae) ->

{

if (btnStartStop.getText().equals("Start"))

{

btnStartStop.setText("Stop");

timer.start();

}

else

{

btnStartStop.setText("Start");

timer.stop();

}

};

btnStartStop.addActionListener(al);

pnl.add(btnStartStop);

setContentPane(pnl);

setSize(300, 80);

setVisible(true);

}

public static void main(String[] args)

{

EventQueue.invokeLater(() -> new Counter("Counter"));

}

}

清单 B-5 的main()方法创建了一个由标签和开始/停止按钮组成的 GUI。标签显示count变量的当前值,按钮文本在开始和停止之间交替。当按钮显示“开始”时,单击该按钮会启动计时器;当按钮显示停止时,单击该按钮会使计时器停止。计时器动作监听器递增count变量,并通过标签显示其值。追加到count的空格字符将表达式转换成一个字符串,并确保其最右边的像素不会被截断。

编译清单 B-5 如下:

javac Counter.java

运行生成的应用,如下所示:

java Counter

图 B-2 显示了最终的 GUI。

A978-1-4842-1700-9_10_Fig2_HTML.jpg

图 B-2。

The panel’s components are horizontally centered

基于计时器的幻灯片放映

幻灯片放映是在投影屏幕上呈现静止图像,通常是按照预先安排的顺序。在被下一个图像替换之前,每个图像通常显示至少几秒钟。

幻灯片放映包括投影仪、屏幕和幻灯片。投影仪包含要投影的幻灯片,屏幕显示投影的幻灯片图像,幻灯片包含图像和其他属性(如文本标题)。

我创建了一个名为SlideShow的 Java 应用,可以让你放映任意的幻灯片。清单 B-6 展示了它的源代码。

Listing B-6. Describing a Timer-Based Slide Show

import java.awt.AlphaComposite;

import java.awt.Color;

import java.awt.Dimension;

import java.awt.EventQueue;

import java.awt.Font;

import java.awt.FontMetrics;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.RenderingHints;

import java.awt.event.ActionListener;

import java.awt.event.WindowAdapter;

import java.awt.event.WindowEvent;

import java.awt.image.BufferedImage;

import java.io.BufferedReader;

import java.io.File;

import java.io.FileReader;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

import javax.imageio.ImageIO;

import javax.swing.JComponent;

import javax.swing.JFrame;

import javax.swing.Timer;

class Projector

{

private volatile List<Slide> slides;

private Screen s;

private Timer t;

private volatile int slideIndexC, slideIndexN;

private volatile float weight;

Projector(List<Slide> slides, Screen s)

{

this.slides = slides;

this.s = s;

t = new Timer(1500, null);

t.setDelay(3000);

slideIndexC = 0;

slideIndexN = 1;

}

void start()

{

s.drawImage(Slide.blend(slides.get(0), null, 1.0f));

ActionListener al = (ae) ->

{

weight = 1.0f;

Timer t2 = new Timer(0, null);

t2.setDelay(10);

ActionListener al2 = (ae2) ->

{

Slide slideC = slides.get(slideIndexC);

Slide slideN = slides.get(slideIndexN);

BufferedImage bi = Slide.blend(slideC, slideN, weight);

s.drawImage(bi);

weight -= 0.01f;

if (weight <= 0.0f)

{

t2.stop();

slideIndexC = slideIndexN;

slideIndexN = (slideIndexN + 1) % slides.size();

}

};

t2.addActionListener(al2);

t2.start();

};

t.addActionListener(al);

t.start();

}

void stop()

{

t.stop();

}

}

class Screen extends JComponent

{

private Dimension d;

private BufferedImage bi;

private String text;

Screen(int width, int height)

{

d = new Dimension(width, height);

}

void drawImage(BufferedImage bi)

{

this.bi = bi;

repaint();

}

@Override

public Dimension getPreferredSize()

{

return d;

}

@Override

public void paint(Graphics g)

{

int w = getWidth();

int h = getHeight();

g.drawImage(bi, Slide.WIDTH <= w ? (w - Slide.WIDTH) / 2 : 0,

Slide.HEIGHT <= h ? (h - Slide.HEIGHT) / 2 : 0, null);

}

}

class Slide

{

static int WIDTH, HEIGHT;

private static int TEXTBOX_WIDTH, TEXTBOX_HEIGHT, TEXTBOX_X, TEXTBOX_Y;

private BufferedImage bi;

private String text;

private static Font font;

private Slide(BufferedImage bi, String text)

{

this.bi = bi;

this.text = text;

font = new Font("Arial", Font.BOLD, 20);

}

static BufferedImage blend(Slide slide1, Slide slide2, float weight)

{

BufferedImage bi1 = slide1.getBufferedImage();

BufferedImage bi2 = (slide2 != null)

? slide2.getBufferedImage()

: new BufferedImage(Slide.WIDTH, Slide.HEIGHT,

BufferedImage.TYPE_INT_RGB);

BufferedImage bi3 = new BufferedImage(Slide.WIDTH, Slide.HEIGHT,

BufferedImage.TYPE_INT_RGB);

Graphics2D g2d = bi3.createGraphics();

g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,

weight));

g2d.drawImage(bi1, 0, 0, null);

g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,

1.0f - weight));

g2d.drawImage(bi2, 0, 0, null);

g2d.setColor(Color.BLACK);

g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,

RenderingHints.VALUE_ANTIALIAS_ON);

g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,

0.5f));

g2d.fillRect(TEXTBOX_X, TEXTBOX_Y, TEXTBOX_WIDTH, TEXTBOX_HEIGHT);

g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,

weight));

g2d.setColor(Color.WHITE);

g2d.setFont(font);

FontMetrics fm = g2d.getFontMetrics();

g2d.drawString(slide1.getText(), TEXTBOX_X + (TEXTBOX_WIDTH -

fm.stringWidth(slide1.getText())) / 2,

TEXTBOX_Y + TEXTBOX_HEIGHT / 2 + fm.getHeight() / 4);

g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,

1.0f - weight));

if (slide2 != null)

g2d.drawString(slide2.getText(), TEXTBOX_X + (TEXTBOX_WIDTH -

fm.stringWidth(slide2.getText())) / 2, TEXTBOX_Y +

TEXTBOX_HEIGHT / 2 + fm.getHeight() / 4);

g2d.dispose();

return bi3;

}

BufferedImage getBufferedImage()

{

return bi;

}

String getText()

{

return text;

}

static List<Slide> loadSlides(String imagesPath) throws IOException

{

File imageFilesPath = new File(imagesPath);

if (!imageFilesPath.isDirectory())

throw new IOException(imagesPath + " identifies a file");

List<Slide> slides = new ArrayList<>();

try (FileReader fr = new FileReader(imagesPath + "/index");

BufferedReader br = new BufferedReader(fr))

{

String line;

while ((line = br.readLine()) != null)

{

String[] parts = line.split(",");

File file = new File(imageFilesPath + "/" + parts[0] + ".jpg");

System.out.println(file);

BufferedImage bi = ImageIO.read(file);

if (WIDTH == 0)

{

WIDTH = bi.getWidth();

HEIGHT = bi.getHeight();

TEXTBOX_WIDTH = WIDTH / 2 + 10;

TEXTBOX_HEIGHT = HEIGHT / 10;

TEXTBOX_Y = HEIGHT - TEXTBOX_HEIGHT - 5;

TEXTBOX_X = (WIDTH - TEXTBOX_WIDTH) / 2;

}

slides.add(new Slide(bi, parts[1]));

}

}

if (slides.size() < 2)

throw new IOException("at least one image must be loaded");

return slides;

}

}

public class SlideShow

{

public static void main(String[] args) throws IOException

{

if (args.length != 1)

{

System.err.println("usage: java SlideShow ssdir");

return;

}

List<Slide> slides = Slide.loadSlides(args[0]);

final Screen screen = new Screen(Slide.WIDTH, Slide.HEIGHT);

final Projector p = new Projector(slides, screen);

Runnable r = () ->

{

final JFrame f = new JFrame("Slide Show");

WindowAdapter wa = new WindowAdapter()

{

@Override

public void windowClosing(WindowEvent we)

{

p.stop();

f.dispose();

}

};

f.addWindowListener(wa);

f.setContentPane(screen);

f.pack();

f.setVisible(true);

p.start();

};

EventQueue.invokeLater(r);

}

}

清单 B-6 根据ProjectorScreenSlideSlideShow类对幻灯片进行建模。Projector声明了几个private字段,一个将投影仪初始化为Slide对象的java.util.List和一个Screen对象的Projector(List<Slide> slides, Screen s)构造器,一个启动投影仪的void start()方法,一个停止投影仪的void stop()方法。

Screen,它子类化javax.swing.JComponent以使一个Screen实例成为一种特殊的 Swing 组件,声明了几个private字段,一个Screen(int width, int height)构造器,用于实例化这个组件到传递给widthheight的屏幕范围,以及一个void drawImage(BufferedImage bi)方法,用于在屏幕表面绘制传递给这个方法的缓冲图像。这个类还覆盖了Dimension getPreferredSize()void paint(Graphics g)来返回组件的首选大小并绘制其表面。

Slide声明了各种常量、几个private字段、一个用于初始化Slide对象的private Slide(BufferedImage bi, String text)构造函数、用于返回幻灯片缓冲图像和文本的BufferedImage getBufferedImage()String getText() getter 方法、一个用于混合一对缓冲图像以显示幻灯片之间过渡的BufferedImage blend(Slide slide1, Slide slide2, float weight)类方法,以及一个用于加载所有幻灯片图像的List<Slide> loadSlides(String imagesPath)类方法。

blend()方法提取与其幻灯片参数相关的缓冲图像,并将这些图像混合在一起,混合量由weight的值决定(必须在0.01.0的范围内)。传递给weight的值越高,slide1的图像对返回的缓冲图像的贡献就越大。混合图像后,blend()在混合图像上混合一对文本字符串。java.awt.AlphaComposite类用于处理每种情况下的混合。

我设计了blend()来处理一种特殊的情况,即null被传递给slide2。这发生在Projectorstart()方法开始时,它执行s.drawImage(Slide.blend(slides.get(0), null, 1.0f));来显示第一张幻灯片——此时没有转换。

loadSlides()方法在由该方法的字符串参数标识的目录中查找名为index的文本文件,并按照该文本文件内容标识的顺序创建SlideList——您可以选择不同于目录中存储的图像文件顺序的幻灯片显示顺序。每一行都被组织成一个文件名,后跟一个逗号,再跟一个文本描述(比如earth, Terran System)。指定文件名时,不要指定文件扩展名;loadSlides()只能识别 JPEG 文件。

SlideShow声明了一个驱动这个应用的main()方法。该方法首先验证已经指定了一个标识幻灯片目录(包含index和 JPEG 文件的目录)的命令行参数。然后它调用loadSlides()从这个目录加载index和所有幻灯片图像。loadSlides()无法加载图像或图像数量少于 2 时抛出java.io.IOException。毕竟,你怎么能有一个少于两张图片的幻灯片呢?

main() next 创建一个用于显示幻灯片图像的Screen组件对象。它将每张幻灯片的宽度和高度(实际上是每张幻灯片图像的宽度和高度)传递给Screen的构造函数,确保屏幕刚好足够显示这些幻灯片。(所有幻灯片图像必须具有相同的宽度和高度,尽管我在loadSlides()中没有强制要求。)

唯一剩下的要创建的重要模型对象是投影仪,main()通过将从loadSlides()返回的Slide对象的List和先前创建的Screen对象传递给Projector的构造函数来完成这项任务。

最后一个任务是在 EDT 上构建 GUI。这个线程将内容窗格设置为Screen对象,并调用Projectorvoid start()方法来开始幻灯片放映。它还创建了一个窗口监听器,当用户试图关闭窗口时,这个监听器调用Projectorvoid stop()方法。然后,该窗口被丢弃。

Projector使用一对Timer对象来管理幻灯片。主定时器对象负责将投影仪推进到下一张幻灯片,从属定时器对象(主定时器每次触发动作事件时创建)负责从当前显示的幻灯片图像过渡到下一张幻灯片的图像(在blend()的帮助下)。

每个定时器实例都在 EDT 上运行。重要的是,当从属定时器运行时,主定时器不执行定时器任务。如果不遵守这条规则,幻灯片放映将会失灵。我选择主定时器任务的连续执行间隔为3000毫秒,从属定时器任务的连续执行间隔为10毫秒,从属定时器任务运行 100 次,总共大约 1000 毫秒。当从属计时器任务完成时,它会自行停止。

编译清单 B-6 如下:

javac SlideShow.java

假设是 Windows 操作系统,运行生成的应用如下:

java SlideShow ..\ss

ss标识示例太阳系幻灯片(包含在本书的代码中),位于当前目录的父目录中。

图 B-3 显示了生成的 GUI。

A978-1-4842-1700-9_10_Fig3_HTML.jpg

图 B-3。

SlideShow horizontally centers a slide’s text near the bottom of the slide