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

78 阅读36分钟

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_​5) contains supplementary material, which is available to authorized users.

前四章关注的是 Java 对线程的底层支持。本章将重点转移到 Java 的高级线程支持上,这就是所谓的并发工具。可以把并发工具想象成用高级语言编写应用,把它的低级线程支持想象成用汇编语言编写应用。在简单地向您介绍了这些工具之后,我将带您参观一下遗嘱执行人。接下来的三章将介绍各种并发工具的其他子集。

并发工具简介

Java 的低级线程支持允许您创建多线程应用,与单线程应用相比,它提供了更好的性能和响应能力。但是,也有问题:

  • 诸如synchronizedwait() / notify()这样的低级并发原语通常很难正确使用。不正确地使用这些原语会导致竞争情况、线程饥饿、死锁和其他危险,这可能很难检测和调试。
  • 过分依赖synchronized原语会导致性能问题,从而影响应用的可伸缩性。对于像 web 服务器这样的高度线程化的应用来说,这是一个很大的问题。
  • 开发人员通常需要更高级别的结构,比如线程池和信号量。因为这些构造不包括在 Java 的底层线程支持中,所以开发人员不得不自己构建,这是一项耗时且容易出错的活动。

为了解决这些问题,Java 5 引入了并发工具,这是一个强大且可扩展的高性能线程工具框架,如线程池和阻塞队列。该框架由以下包中的各种类型组成:

  • java.util.concurrent:并发编程中经常用到的工具类型,比如执行器。
  • java.util.concurrent.atomic:支持单变量无锁线程安全编程的工具类。
  • java.util.concurrent.locks:根据条件锁定和等待的工具类型(让线程暂停执行[等待]直到被其他线程通知某个布尔状态现在可能为真的对象)。通过这些类型的锁定和等待比通过 Java 的基于监视器的同步和等待/通知机制更具性能和灵活性。

这个框架还向java.lang.System类引入了一个long nanoTime()方法,它允许您访问纳秒粒度的时间源来进行相对时间测量。

并发工具可以分为执行器、同步器、锁定框架等等。我将在下一节探讨遗嘱执行人,并在后续章节中探讨这些类别。

探索执行者

Threads API 允许您通过像new java.lang.Thread (new RunnableTask()).start();这样的表达式来执行可运行的任务。这些表达式将任务提交与任务的执行机制紧密耦合(在当前线程、新线程或从线程池[组]中任意选择的线程上运行)。

Note

任务是一个对象,它的类实现了java.lang.Runnable接口(一个可运行的任务)或java.util.concurrent.Callable接口(一个可调用的任务)。在这一章的后面我会说更多关于Callable的事情。

并发工具包括执行器,作为执行可运行任务的低级线程表达式的高级替代。执行器是一个对象,它的类直接或间接地实现了java.util.concurrent.Executor接口,该接口将任务提交与任务执行机制相分离。

Note

Executor 框架使用接口将任务提交从任务执行中分离出来,类似于 Collections 框架使用核心接口将列表、集合、队列和映射从它们的实现中分离出来。解耦产生了更容易维护的灵活代码。

Executor声明了一个单独的void execute(Runnable runnable)方法,该方法在未来的某个时间执行名为runnable的可运行任务。execute()runnablenull时抛出java.lang.NullPointerException,不能执行runnable时抛出java.util.concurrent.RejectedExecutionException

Note

当一个执行程序正在关闭并且不想接受新的任务时会抛出。此外,当执行器没有足够的空间来存储任务时,也会抛出这个异常(也许执行器使用了一个有界的阻塞队列来存储任务,而队列已经满了——我在第八章中讨论了阻塞队列)。

下面的例子给出了前面提到的new Thread(new RunnableTask()).start();表达式的Executor等价物:

Executor executor = ...; //  ... represents some executor creation

executor.execute(new RunnableTask());

虽然Executor很容易使用,但是这个接口在各方面都有限制:

  • Executor只关注Runnable。因为Runnablerun()方法不返回值,所以对于一个可运行的任务来说,没有简单的方法向它的调用者返回值。
  • Executor没有提供一种方法来跟踪正在执行的可运行任务的进度,取消正在执行的可运行任务,或者确定可运行任务何时完成执行。
  • Executor无法执行可运行任务的集合。
  • 没有为应用提供关闭执行程序的方法(更不用说正确关闭执行程序了)。

这些限制由java.util.concurrent.ExecutorService接口解决,该接口扩展了Executor,其实现通常是一个线程池。表 5-1 描述了ExecutorService的方法。

表 5-1。

ExecutorService’s Methods

| 方法 | 描述 | | --- | --- | | `boolean awaitTermination(long timeout, TimeUnit unit)` | 阻塞(等待)直到关闭请求后所有任务都已完成,`timeout`(以`unit`时间单位测量)到期,或者当前线程被中断,无论哪种情况先发生。当该执行人终止时返回`true`,当`timeout`在终止前结束时返回`false`。该方法中断时抛出`java.lang.InterruptedException`。 | | ` List> invokeAll(Collection> tasks)` | 执行`tasks`集合中的每个可调用任务,并返回一个`java.util.concurrent.Future`实例的`java.util.List`(将在本章后面讨论),当所有任务完成时,这些实例保存任务状态和结果——任务通过正常终止或抛出异常来完成。`Future` s 的`List`与`tasks`迭代器返回的任务顺序相同。当这个方法在等待中被中断时抛出`InterruptedException`,在这种情况下,未完成的任务被取消;`NullPointerException`当`tasks`或其任意元素为`null`时;以及当任何一个`tasks`任务不能被调度执行时的`RejectedExecutionException`。 | | ` List> invokeAll(Collection> tasks, long timeout, TimeUnit unit)` | 执行`tasks`集合中的每个可调用任务,并返回一个`Future`实例的`List`,当所有任务完成时(通过正常终止或抛出异常来完成任务)或`timeout`(以`unit`时间单位度量)到期时,这些实例保存任务状态和结果。到期时未完成的任务将被取消。`Future` s 的`List`与`tasks`迭代器返回的任务顺序相同。这个方法在等待中被中断时抛出`InterruptedException`(未完成的任务被取消)。当`tasks`、其任意元素或`unit`为`null`时,它也会抛出`NullPointerException`;并且当任何一个`tasks`任务不能被调度执行时抛出`RejectedExecutionException`。 | | ` T invokeAny(Collection> tasks)` | 执行给定的`tasks`,返回成功完成的任意任务的结果(换句话说,没有抛出异常),如果有的话。在正常或异常返回时,未完成的任务被取消。该方法在等待过程中被中断时抛出`InterruptedException`,当`tasks`或其任何元素为`null`时抛出`NullPointerException`,当`tasks`为空时抛出`java.lang.IllegalArgumentException`,当没有任务成功完成时抛出`java.util.concurrent.ExecutionException`,当没有任务可以被调度执行时抛出`RejectedExecutionException`。 | | ` T invokeAny(Collection> tasks, long timeout, TimeUnit unit)` | 执行给定的`tasks`,返回成功完成的任意任务的结果(没有抛出异常),如果在`timeout`(以`unit`时间单位测量)到期之前有任何任务成功完成,则取消到期时未完成的任务。在正常或异常返回时,未完成的任务将被取消。该方法在等待过程中被中断时抛出`InterruptedException`;`NullPointerException`当`tasks`时,其任一元素,或`unit`为`null`;`IllegalArgumentException`当`tasks`为空时;`java.util.concurrent.TimeoutException`当`timeout`在任何任务成功完成之前过去时;`ExecutionException`没有任务成功完成时;以及`RejectedExecutionException`当没有任务可以被调度执行时。 | | `boolean isShutdown()` | 当该执行程序被关闭时返回`true`;否则,返回`false`。 | | `boolean isTerminated()` | 关机后所有任务完成后返回`true`;否则,返回`false`。在调用`shutdown()`或`shutdownNow()`之前,这个方法永远不会返回`true`。 | | `void shutdown()` | 启动有序关机,执行以前提交的任务,但不接受新任务。执行程序关闭后,调用此方法没有任何效果。该方法不等待先前提交的任务完成执行。当需要等待时,使用`awaitTermination()`。 | | `List shutdownNow()` | 尝试停止所有正在执行的任务,暂停正在等待的任务的处理,并返回正在等待执行的任务列表。除了尽最大努力停止处理正在执行的任务之外,没有任何保证。例如,典型的实现将通过`Thread.interrupt()`取消,因此任何未能响应中断的任务可能永远不会终止。 | | ` Future submit(Callable task)` | 提交一个可调用的`task`来执行,并返回一个代表`task`的未决结果的`Future`实例。`Future`实例的`get()`方法在成功完成时返回`task`的结果。当`task`不能被调度执行时,该方法抛出`RejectedExecutionException`,当`task`为`null`时,抛出`NullPointerException`。如果您想在等待任务完成时立即阻塞,您可以使用形式为`result = exec.submit(aCallable).get();`的结构。 | | `Future submit(Runnable task)` | 提交一个可运行的`task`来执行,并返回一个代表`task`的未决结果的`Future`实例。`Future`实例的`get()`方法在成功完成时返回`task`的结果。当`task`不能被调度执行时,该方法抛出`RejectedExecutionException`,当`task`为`null`时,抛出`NullPointerException`。 | | ` Future submit(Runnable task, T result)` | 提交一个可运行的`task`来执行,并返回一个`Future`实例,其`get()`方法在成功完成时返回`result`的值。该方法在`task`不能被调度执行时抛出`RejectedExecutionException`,在`task`为`null`时抛出`NullPointerException`。 |

表 5-1 是指java.util.concurrent.TimeUnit,一个以给定粒度单位表示持续时间的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS。此外,TimeUnit声明了跨单元转换的方法(如long toHours(long duration)),以及在这些单元中执行定时和延迟操作的方法(如void sleep(long timeout))。

表 5-1 也指可调用任务。与Runnable不同,它的void run()方法不能返回值和抛出被检查的异常,Callable<V>V call()方法返回值并能抛出被检查的异常,因为它是用throws Exception子句声明的。

最后,表 5-1 引用了Future接口,它代表了异步计算的结果。结果被称为未来,因为它通常要到未来的某个时刻才可用。Future,其泛型类型为Future<V>,提供了取消任务、返回任务值以及确定任务是否完成的方法。表 5-2 描述了Future的方法。

表 5-2。

Future’s Methods

| 方法 | 描述 | | --- | --- | | `boolean cancel(boolean mayInterruptIfRunning)` | 尝试取消此任务的执行,并在任务取消时返回`true`;否则,返回`false`(任务可能在`cancel()`被调用之前已经正常完成)。当任务已完成、已取消或由于其他原因无法取消时,取消会失败。如果成功,并且该任务尚未开始,则该任务不应运行。如果任务已经开始,`mayInterruptIfRunning`确定是否(`true`)应该中断运行该任务的线程以试图停止该任务。返回后,后续对`isDone()`的调用总是返回`true`;`isCancelled()`当`cancel()`返回`true`时,总是返回`true`。 | | `V get()` | 如果需要,等待任务完成,然后返回结果。当任务在这个方法被调用之前被取消时,这个方法抛出`java.util.concurrent.CancellationException`,当任务抛出异常时抛出`ExecutionException`,当当前线程在等待时被中断时抛出`InterruptedException`。 | | `V get(long timeout, TimeUnit unit)` | 等待最多`timeout`个单位(由`unit`指定)来完成任务,然后返回结果(如果可用)。当任务在该方法被调用之前被取消时,该方法抛出`CancellationException`,当任务抛出异常时抛出`ExecutionException`,当当前线程在等待时被中断时抛出`InterruptedException`,当该方法的`timeout`值到期(等待超时)时抛出`TimeoutException`。 | | `boolean isCancelled()` | 当该任务在正常完成前被取消时,返回`true`;否则,返回`false`。 | | `boolean isDone()` | 该任务完成后返回`true`;否则,返回`false`。完成可能是由于正常终止、异常或取消——在所有这些情况下,该方法都返回`true`。 |

假设您打算编写一个应用,它的图形用户界面允许用户输入单词。用户输入单词后,应用将这个单词呈现给几个在线词典,并获得每个词典的条目。这些条目随后显示给用户。

因为在线访问可能很慢,而且用户界面应该保持响应(也许用户想要结束应用),所以您将“获取单词条目”任务卸载到一个在单独线程上运行该任务的执行器。以下示例使用ExecutorServiceCallableFuture来实现这一目标:

ExecutorService executor = ...; // ... represents some executor creation

Future<String[]> taskFuture =

executor.submit(new Callable<String[]>()

{

@Override

public String[] call()

{

String[] entries = ...;

// Access online dictionaries

// with search word and populate

// entries with their resulting

// entries.

return entries;

}

});

// Do stuff.

String entries = taskFuture.get();

在以某种方式获得一个执行程序后(您将很快了解如何获得),该示例的线程向执行程序提交一个可调用的任务。submit()方法立即返回一个对用于控制任务执行和访问结果的Future对象的引用。线程最终调用这个对象的get()方法来获得这些结果。

Note

java.util.concurrent.ScheduledExecutorService接口扩展了ExecutorService,并描述了一个执行器,让您调度任务运行一次或在给定延迟后定期执行。

虽然您可以创建自己的ExecutorExecutorServiceScheduledExecutorService实现(比如class DirectExecutor implements Executor { @Override public void execute(Runnable r) { r.run(); } }—直接在调用线程上运行 executor),但是还有一个更简单的选择:java.util.concurrent.Executors

Tip

如果您打算创建自己的ExecutorService实现,您会发现使用java.util.concurrent.AbstractExecutorServicejava.util.concurrent.FutureTask类会很有帮助。

Executors工具类声明了几个类方法,这些方法返回各种ExecutorServiceScheduledExecutorService实现的实例(以及其他类型的实例)。这个类的static方法完成以下任务:

  • 创建并返回一个用常用配置设置配置的ExecutorService实例。
  • 创建并返回一个用常用配置设置配置的ScheduledExecutorService实例。
  • 创建并返回一个“包装的”ExecutorServiceScheduledExecutorService实例,通过使特定于实现的方法不可访问来禁用执行器服务的重新配置。
  • 创建并返回一个java.util.concurrent.ThreadFactory实例(即实现了ThreadFactory接口的类的实例),用于创建新的Thread对象。
  • 从其他类似闭包的形式中创建并返回一个Callable实例,这样它就可以用在需要Callable参数的执行方法中(比如ExecutorServicesubmit(Callable)方法)。维基百科 http://en.wikipedia.org/wiki/Closure_(computer_science) 的“闭包(计算机科学)”条目介绍了闭包的主题。

例如,static ExecutorService newFixedThreadPool(int nThreads)创建一个线程池,它重用固定数量的线程,这些线程在一个共享的无界队列中运行。最多nThreads线程主动处理任务。如果在所有线程都处于活动状态时提交了额外的任务,它们将在队列中等待一个可用的线程。

如果在执行器关闭之前,任何线程由于执行过程中的故障而终止,那么在需要执行后续任务时,一个新的线程将取代它的位置。在明确关闭执行器之前,线程池中的线程将一直存在。当您将零或负值传递给nThreads时,该方法抛出IllegalArgumentException

Note

线程池用于消除为每个提交的任务创建新线程的开销。线程创建并不便宜,而且必须创建许多线程会严重影响应用的性能。

您通常会在文件和网络输入/输出上下文中使用执行器、可运行对象、可调用程序和未来程序。执行冗长的计算提供了使用这些类型的另一个场景。例如,清单 5-1 在欧拉数 e (2.71828)的计算上下文中使用了一个执行器、一个可调用函数和一个未来值...).

Listing 5-1. Calculating Euler’s Number e

import java.math.BigDecimal;

import java.math.MathContext;

import java.math.RoundingMode;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

public class CalculateE

{

final static int LASTITER = 17;

public static void main(String[] args)

{

ExecutorService executor = Executors.newFixedThreadPool(1);

Callable<BigDecimal> callable;

callable = new Callable<BigDecimal>()

{

@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;

}

public BigDecimal factorial(BigDecimal n)

{

if (n.equals(BigDecimal.ZERO))

return BigDecimal.ONE;

else

return n.multiply(factorial(n.

subtract(BigDecimal.ONE)));

}

};

Future<BigDecimal> taskFuture = executor.submit(callable);

try

{

while (!taskFuture.isDone())

System.out.println("waiting");

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

}

catch(ExecutionException ee)

{

System.err.println("task threw an exception");

System.err.println(ee);

}

catch(InterruptedException ie)

{

System.err.println("interrupted while waiting");

}

executor.shutdownNow();

}

}

执行main()的默认主线程首先通过调用Executors ' newFixedThreadPool()方法获得一个执行器。然后,它实例化一个实现了Callable接口的匿名类,并将这个任务提交给执行器,接收一个Future实例作为响应。

提交任务后,线程通常会做一些其他工作,直到它需要任务的结果。我通过让主线程重复输出等待消息来模拟这项工作,直到Future实例的isDone()方法返回true。(在实际应用中,我会避免这种循环。)此时,主线程调用实例的get()方法获得结果,然后输出结果。然后主线程关闭执行器。

Caution

在执行程序完成后关闭它是很重要的;否则,应用可能不会结束。前一个执行者通过调用shutdownNow()来完成这个任务。(您也可以使用shutdown()方法。)

callable 的call()方法通过计算数学幂级数 e = 1 / 0 来计算 e!+ 1 / 1!+ 1 / 2!+ .。。。这个级数可以用求和 1 / n 来求值!,其中 n 的范围是从 0 到无穷大(并且!代表阶乘)。

call()首先实例化java.math.MathContext封装一个精度(位数)和一个舍入方式。我选择了100作为 e 的精度上限,也选择了HALF_UP作为舍入模式。

Tip

增加精度和LASTITER的值,使级数收敛到更长更精确的 e 的近似值。

call()接下来将名为result的局部变量java.math.BigDecimal初始化为BigDecimal.ZERO。然后它进入一个循环,计算阶乘,用阶乘除BigDecimal.ONE,并将除法结果加到result

divide()方法将MathContext实例作为其第二个参数来提供舍入信息。(如果我将0指定为数学上下文的精度和一个非终止的十进制展开[除法的商结果不能精确地表示为-0.3333333...例如]发生时,java.lang.ArithmeticException将被抛出以警告调用者商不能被精确表示的事实。遗嘱执行人会以ExecutionException的名义重新抛出这个例外。)

编译清单 5-1 如下:

javac CalculateE.java

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

java CalculateE

您应该观察到类似如下的输出(您可能会观察到更多的waiting消息):

waiting

waiting

waiting

waiting

waiting

2.718281828459045070516047795848605061178979635251032698900735004065225042504843314055887974344245741730039454062711

Exercises

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

What are the concurrency utilities?   Identify the packages in which the concurrency utilities types are stored.   Define task.   Define executor.   Identify the Executor interface’s limitations.   How are Executor’s limitations overcome?   What differences exist between Runnable’s run( ) method and Callable’s call( ) method?   True or false: You can throw checked and unchecked exceptions from Runnable’s run( ) method but can only throw unchecked exceptions from Callable’s call( ) method.   Define future.   Describe the Executors class’s newFixedThreadPool( ) method.   Refactor the following CountingThreads application to work with Executors and 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++);                       }                    };       Thread thdA = new Thread(r);       Thread thdB = new Thread(r);       thdA.start();       thdB.start();    } }   When you execute the previous exercise’s CountingThreads application, you’ll observe output that identifies the threads via names such as pool-1-thread-1. Modify CountingThreads so that you observe names A and B. Hint: You’ll need to use ThreadFactory.  

摘要

Java 的低级线程功能使您可以创建多线程应用,这些应用比单线程应用提供更好的性能和响应能力。然而,影响应用可伸缩性的性能问题和其他问题导致 Java 5 引入了并发工具。

并发工具将各种类型组织成三个包:java.util.concurrentjava.util.concurrent.atomicjava.util.concurrent.locks。执行器、线程池、并发哈希表和其他高级并发结构的基本类型存储在java.util.concurrent中;支持单变量无锁、线程安全编程的类存储在java.util.concurrent.atomic;锁定和等待条件的类型存储在java.util.concurrent.locks中。

执行器将任务提交从任务执行机制中分离出来,由ExecutorExecutorServiceScheduledExecutorService接口描述。您可以通过调用Executors类中的一个实用方法来获得一个执行器。遗嘱执行人与可赎回和期货相关联。

第六章介绍同步器。

六、同步器

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

Java 提供了synchronized关键字来同步线程对临界区的访问。因为很难正确编写基于synchronized的同步代码,所以在并发工具中包含了高级同步器(促进常见形式的同步的类)。在这一章中,我将向您介绍倒计时锁、循环屏障、交换器、信号量和相位同步器。

倒计时闩锁

倒计时锁存导致一个或多个线程在“门”处等待,直到另一个线程打开这个门,此时这些其他线程可以继续。它由一个计数和“使线程等待,直到计数达到零”以及“递减计数”的操作组成

java.util.concurrent.CountDownLatch类实现了倒计时锁存同步器。通过调用这个类的CountDownLatch(int count)构造函数,将一个CountDownLatch实例初始化为一个特定的计数,当传递给count的值为负时,这个构造函数抛出java.lang.IllegalArgumentException

CountDownLatch还提供了以下方法:

  • void await():强制调用线程等待,直到锁存器已经倒计数到零,除非线程被中断,在这种情况下抛出java.lang.InterruptedException。当计数为零时,此方法立即返回。
  • boolean await(long timeout, TimeUnit unit):强制调用线程等待,直到锁存器已经倒计数到零,或者unit时间单位中指定的timeout值已经到期,或者线程被中断,在这种情况下InterruptedException被抛出。当计数为零时,此方法立即返回。当计数达到零时,它返回true,或者当等待时间过去时,它返回false
  • void countDown():递减计数,当计数达到零时,释放所有等待线程。调用此方法时,如果计数已经为零,则不会发生任何事情。
  • long getCount():返回当前计数。此方法对于测试和调试非常有用。
  • String toString():返回一个标识该闩锁及其状态的字符串。括号中的状态包括字符串文字"Count =",后跟当前计数。

您通常会使用一个倒计时闩锁来确保线程几乎同时开始工作。例如,查看清单 6-1 。

Listing 6-1. Using a Countdown Latch to Trigger a Coordinated Start

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class CountDownLatchDemo

{

final static int NTHREADS = 3;

public static void main(String[] args)

{

final CountDownLatch startSignal = new CountDownLatch(1);

final CountDownLatch doneSignal = new CountDownLatch(NTHREADS);

Runnable r = new Runnable()

{

@Override

public void run()

{

try

{

report("entered run()");

startSignal.await();  // wait until told to ...

report("doing work"); // ... proceed

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

doneSignal.countDown(); // reduce count on which

// main thread is ...

}                          // waiting

catch (InterruptedException ie)

{

System.err.println(ie);

}

}

void report(String s)

{

System.out.println(System.currentTimeMillis() +

": " + Thread.currentThread() +

": " + s);

}

};

ExecutorService executor = Executors.newFixedThreadPool(NTHREADS);

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

executor.execute(r);

try

{

System.out.println("main thread doing something");

Thread.sleep(1000); // sleep for 1 second

startSignal.countDown(); // let all threads proceed

System.out.println("main thread doing something else");

doneSignal.await(); // wait for all threads to finish

executor.shutdownNow();

}

catch (InterruptedException ie)

{

System.err.println(ie);

}

}

}

清单 6-1 的默认主线程首先创建一对倒计时锁存器。startSignal倒计时闩锁阻止任何工作线程继续运行,直到默认主线程准备好让它们继续运行。doneSignal倒计时闩锁导致默认主线程等待,直到所有工作线程完成。

默认主线程接下来用一个run()方法创建一个 runnable,该方法由随后创建的工作线程执行。

run()首先输出一条消息,然后调用startSignalawait()方法,在继续之前等待这个倒计时锁存器的计数读取零,此时run()输出一条消息,指示工作正在进行,并休眠一段随机的时间(0 到 999 毫秒)来模拟这项工作。

此时,run()调用doneSignalcountDown()方法来减少这个锁存器的计数。一旦该计数达到零,等待该信号的默认主线程将继续,关闭执行器并终止应用。

在创建 runnable 之后,默认主线程获得一个基于NTHREADS线程的线程池的执行程序,然后调用执行程序的execute()方法NTHREADS次,将 runnable 传递给每个基于NTHREADS池的线程。这个动作启动进入run()的工作线程。

接下来,默认主线程输出一条消息并休眠一秒钟,以模拟做额外的工作(给所有工作线程一个进入run()并调用startSignal.await()的机会),调用startSignalcountDown()方法以使工作线程开始运行,输出一条消息以指示它正在做其他事情,并调用doneSignalawait()方法以等待这个倒计时闩锁的计数达到零,然后才能继续。

编译清单 6-1 如下:

javac CountDownLatchDemo.java

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

java CountDownLatchDemo

您应该观察到类似于以下内容的输出(消息顺序可能有所不同):

main thread doing something

1445802274931: Thread[pool-1-thread-2,5,main]: entered run()

1445802274931: Thread[pool-1-thread-3,5,main]: entered run()

1445802274931: Thread[pool-1-thread-1,5,main]: entered run()

main thread doing something else

1445802275931: Thread[pool-1-thread-2,5,main]: doing work

1445802275931: Thread[pool-1-thread-3,5,main]: doing work

1445802275933: Thread[pool-1-thread-1,5,main]: doing work

环状屏障

循环障碍让一组线程相互等待到达一个公共障碍点。屏障是循环的,因为它可以在等待线程被释放后被重用。这种同步器在应用中非常有用,这些应用包含一组固定大小的线程,它们偶尔会相互等待。

java.util.concurrent.CyclicBarrier类实现了循环屏障同步器。通过调用这个类的CyclicBarrier(int parties)构造函数,将一个CyclicBarrier实例初始化为特定数量的参与方(为一个共同目标工作的线程)。当传递给parties的值小于 1 时,这个构造函数抛出IllegalArgumentException

或者,您可以调用CyclicBarrier(int parties, Runnable barrierAction)构造函数将循环屏障初始化为特定数量的parties和一个barrierAction,当屏障被触发时执行。换句话说,当parties - 1线程正在等待并且又有一个线程到达时,到达的线程执行barrierAction,然后所有线程继续。这个 runnable 对于在任何线程继续之前更新共享状态非常有用。当传递给parties的值小于 1 时,这个构造函数抛出IllegalArgumentException。(前一个构造函数调用这个构造函数,将null传递给barrierAction——当障碍被触发时,将不执行任何 runnable。)

CyclicBarrier还提供了以下方法:

  • int await():强制调用线程等待,直到各方都调用了此循环屏障上的await()。当它或另一个等待线程被中断,另一个线程等待超时,或者另一个线程在这个循环屏障上调用reset()时,调用线程也会停止等待。如果调用线程在入口设置了中断状态,或者在等待时被中断,该方法抛出InterruptedException,调用线程的中断状态被清除。当任何线程正在等待时屏障被重置(通过reset())时,该方法抛出java.util.concurrent.BrokenBarrierException,或者当await()被调用或任何线程正在等待时屏障被破坏。当任何一个线程在等待时被中断,所有其他等待的线程抛出BrokenBarrierException,屏障被置于中断状态。如果调用线程是最后到达的线程,并且在构造函数中提供了 non-??,那么调用线程在允许其他线程继续之前执行这个 runnable。这个方法返回调用线程的到达索引,其中索引getParties() - 1表示第一个到达的线程,零表示最后一个到达的线程。
  • int await(long timeout, TimeUnit unit):这个方法等同于前面的方法,除了它让你指定调用线程愿意等待多长时间。当线程等待超时时,这个方法抛出java.util.concurrent.TimeoutException
  • int getNumberWaiting():返回当前在关卡等待的队伍数量。此方法对于调试和与断言的合作非常有用。
  • int getParties():返回需要穿越障碍的队伍数量。
  • boolean isBroken():当一方或多方由于循环屏障构建或最后一次重置后的中断或超时而突破此屏障时,或当屏障动作因异常而失败时,返回true;否则,返回false
  • void reset():将栅栏复位到初始状态。如果有任何一方正在关卡处等待,他们将返回一个BrokenBarrierException。请注意,由于其他原因发生破损后,重置操作可能会很复杂;线程需要以其他方式重新同步,并选择一个线程来执行重置。因此,最好为后续使用创建一个新的屏障。

循环障碍在并行分解场景中很有用,在并行分解场景中,一个冗长的任务被分成多个子任务,这些子任务的单个结果随后被合并到任务的整体结果中。CyclicBarrier的 Javadoc 展示了清单 6-2 中完成的示例代码。

Listing 6-2. Using a Cyclic Barrier to Decompose a Task into Subtasks

import java.util.concurrent.BrokenBarrierException;

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo

{

public static void main(String[] args)

{

float[][] matrix = new float[3][3];

int counter = 0;

for (int row = 0; row < matrix.length; row++)

for (int col = 0; col < matrix[0].length; col++)

matrix[row][col] = counter++;

dump(matrix);

System.out.println();

Solver solver = new Solver(matrix);

System.out.println();

dump(matrix);

}

static void dump(float[][] matrix)

{

for (int row = 0; row < matrix.length; row++)

{

for (int col = 0; col < matrix[0].length; col++)

System.out.print(matrix[row][col] + " ");

System.out.println();

}

}

}

class Solver

{

final int N;

final float[][] data;

final CyclicBarrier barrier;

class Worker implements Runnable

{

int myRow;

boolean done = false;

Worker(int row)

{

myRow = row;

}

boolean done()

{

return done;

}

void processRow(int myRow)

{

System.out.println("Processing row: " + myRow);

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

data[myRow][i] *= 10;

done = true;

}

@Override

public void run()

{

while (!done())

{

processRow(myRow);

try

{

barrier.await();

}

catch (InterruptedException ie)

{

return;

}

catch (BrokenBarrierException bbe)

{

return;

}

}

}

}

public Solver(float[][] matrix)

{

data = matrix;

N = matrix.length;

barrier = new CyclicBarrier(N,

new Runnable()

{

@Override

public void run()

{

mergeRows();

}

});

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

new Thread(new Worker(i)).start();

waitUntilDone();

}

void mergeRows()

{

System.out.println("merging");

synchronized("abc")

{

"abc".notify();

}

}

void waitUntilDone()

{

synchronized("abc")

{

try

{

System.out.println("main thread waiting");

"abc".wait();

System.out.println("main thread notified");

}

catch (InterruptedException ie)

{

System.out.println("main thread interrupted");

}

}

}

}

清单 6-2 的默认主线程首先创建一个浮点值的方阵,并将这个矩阵转储到标准输出流。这个线程然后实例化Solver类,该类创建一个单独的线程来对每一行执行计算。然后,修改后的矩阵被转储。

Solver表示一个构造函数,该构造函数接收它的matrix参数,并将它的引用保存在字段data中,并将行数保存在字段N中。然后,构造函数创建一个包含N方的循环屏障和一个负责将所有行合并成最终矩阵的屏障动作。最后,构造函数创建一个工作线程,执行一个单独的Worker runnable,负责处理矩阵中的一行。构造函数然后等待,直到工作线程完成。

Worker 的run()方法在其特定的行上重复调用processRow(),直到done()返回true,在processRow()执行一次后,它就会返回true(在本例中)。在processRow()返回后,表明该行已经被处理,工作线程在循环屏障上调用await();它不能继续。

在某个时刻,所有的工作线程都会调用await()。当处理矩阵中最后一行的最后一个线程调用await()时,它将触发 barrier 动作,将所有处理过的行合并到一个最终矩阵中。在这个例子中,不需要合并,但是在更复杂的例子中需要合并。

mergeRows()执行的最后一个任务是通知调用Solver的构造函数的主线程。该线程正在等待与String对象"abc"相关联的监视器。对notify()的调用足以唤醒正在等待的线程,它是这个监视器上唯一正在等待的线程。

编译清单 6-2 如下:

javac CyclicBarrierDemo.java

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

java CyclicBarrierDemo

您应该观察到类似于以下内容的输出(消息顺序可能有所不同):

0.0 1.0 2.0

3.0 4.0 5.0

6.0 7.0 8.0

main thread waiting

Processing row: 0

Processing row: 1

Processing row: 2

merging

main thread notified

0.0 10.0 20.0

30.0 40.0 50.0

60.0 70.0 80.0

交换器

交换器提供了一个同步点,线程可以在这里交换对象。每个线程在交换器的exchange()方法的入口提供一些对象,与一个伙伴线程匹配,并在返回时接收其伙伴的对象。交换器在遗传算法(见 http://en.wikipedia.org/wiki/Genetic_algorithm )和管道设计等应用中非常有用。

通用的java.util.concurrent.Exchanger<V>类实现了交换器同步器。您可以通过调用Exchanger()构造函数来初始化一个交换器。然后,您可以调用以下任一方法来执行交换:

  • V exchange(V x):等待另一个线程到达这个交换点(除非调用线程被中断),然后将给定的对象传递给它,作为回报接收另一个线程的对象。如果另一个线程已经在交换点等待,它将继续执行线程调度,并接收调用线程传入的对象。当前线程立即返回,接收另一个线程传递给交换器的对象。这个方法在调用线程中断时抛出InterruptedException
  • V exchange(V x, long timeout, TimeUnit unit):这个方法等同于前面的方法,除了它让你指定调用线程愿意等待多长时间。当线程等待超时时,它抛出TimeoutException

清单 6-3 扩展了Exchanger的 Javadoc 中给出的重复填充和清空Exchanger的例子。

Listing 6-3. Using an Exchanger to Swap Buffers

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Exchanger;

public class ExchangerDemo

{

final static Exchanger<DataBuffer> exchanger =

new Exchanger<DataBuffer>();

final static DataBuffer initialEmptyBuffer = new DataBuffer();

final static DataBuffer initialFullBuffer = new DataBuffer("I");

public static void main(String[] args)

{

class FillingLoop implements Runnable

{

int count = 0;

@Override

public void run()

{

DataBuffer currentBuffer = initialEmptyBuffer;

try

{

while (true)

{

addToBuffer(currentBuffer);

if (currentBuffer.isFull())

{

System.out.println("filling thread wants to exchange");

currentBuffer = exchanger.exchange(currentBuffer);

System.out.println("filling thread receives exchange");

}

}

}

catch (InterruptedException ie)

{

System.out.println("filling thread interrupted");

}

}

void addToBuffer(DataBuffer buffer)

{

String item = "NI" + count++;

System.out.println("Adding: " + item);

buffer.add(item);

}

}

class EmptyingLoop implements Runnable

{

@Override

public void run()

{

DataBuffer currentBuffer = initialFullBuffer;

try

{

while (true)

{

takeFromBuffer(currentBuffer);

if (currentBuffer.isEmpty())

{

System.out.println("emptying thread wants to " +

"exchange");

currentBuffer = exchanger.exchange(currentBuffer);

System.out.println("emptying thread receives " +

"exchange");

}

}

}

catch (InterruptedException ie)

{

System.out.println("emptying thread interrupted");

}

}

void takeFromBuffer(DataBuffer buffer)

{

System.out.println("taking: " + buffer.remove());

}

}

new Thread(new EmptyingLoop()).start();

new Thread(new FillingLoop()).start();

}

}

class DataBuffer

{

private final static int MAXITEMS = 10;

private final List<String> items = new ArrayList<>();

DataBuffer()

{

}

DataBuffer(String prefix)

{

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

{

String item = prefix + i;

System.out.printf("Adding %s%n", item);

items.add(item);

}

}

synchronized void add(String s)

{

if (!isFull())

items.add(s);

}

synchronized boolean isEmpty()

{

return items.size() == 0;

}

synchronized boolean isFull()

{

return items.size() == MAXITEMS;

}

synchronized String remove()

{

if (!isEmpty())

return items.remove(0);

return null;

}

}

清单 6-3 的默认主线程通过静态字段初始化器创建一个交换器和一对缓冲区。然后实例化EmptyingLoopFillingLoop局部类,并将这些可运行的类传递给新的Thread实例,然后启动这些实例的线程。(我本来可以用遗嘱执行人。)每个 runnable 的run()方法进入一个无限循环,重复地增加或删除它的缓冲区。当缓冲液满或空时,交换器用于交换这些缓冲液,继续填充或清空。

编译清单 6-3 如下:

javac ExchangerDemo.java

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

java ExchangerDemo

您应该观察到类似如下的输出前缀(消息顺序可能有所不同):

Adding I0

Adding I1

Adding I2

Adding I3

Adding I4

Adding I5

Adding I6

Adding I7

Adding I8

Adding I9

taking: I0

taking: I1

taking: I2

taking: I3

taking: I4

taking: I5

taking: I6

taking: I7

taking: I8

taking: I9

emptying thread wants to exchange

Adding: NI0

Adding: NI1

Adding: NI2

Adding: NI3

Adding: NI4

Adding: NI5

Adding: NI6

Adding: NI7

Adding: NI8

Adding: NI9

filling thread wants to exchange

filling thread receives exchange

emptying thread receives exchange

Adding: NI10

taking: NI0

Adding: NI11

taking: NI1

Adding: NI12

旗语

信号量维护一组许可证,用于限制可以访问有限资源的线程数量。当没有许可可用时,试图获取许可的线程将被阻塞,直到某个其他线程释放许可。

Note

当前值可以递增到 1 以上的信号量称为计数信号量,而当前值只能为 0 或 1 的信号量称为二进制信号量或互斥信号量。无论哪种情况,当前值都不能为负。

java.util.concurrent.Semaphore类实现了这个同步器,并将信号量概念化为维护一组许可的对象。通过调用Semaphore(int permits)构造函数来初始化信号量,其中permits指定了可用许可的数量。结果信号量的公平策略被设置为false(不公平)。或者,您可以调用Semaphore(int permits, boolean fair)构造函数来将信号量的公平性设置为true (fair)。

Semaphores and Fairness

当公平性设置为false时,Semaphore不保证线程获取许可的顺序。特别是,驳船是允许的;也就是说,调用acquire()的线程可以在已经等待的线程之前被分配一个许可——逻辑上,新线程将自己放在等待线程队列的最前面。当fair被设置为true时,信号量保证调用任何acquire()方法的线程被选择来按照它们对这些方法的调用被处理的顺序获得许可(先进先出;FIFO)。因为 FIFO 排序必须应用于这些方法中特定的内部执行点,所以一个线程可能在另一个线程之前调用acquire(),但在另一个线程之后到达排序点,从方法返回时也是如此。此外,不计时的tryAcquire()方法不尊重公平设置;他们会拿走所有可用的许可证。

一般来说,用于控制资源访问的信号量应该初始化为 fair,以确保没有线程会因饥饿而无法访问资源。当使用信号量进行其他类型的同步控制时,不公平排序的吞吐量优势通常超过公平性考虑。

Semaphore还提供了以下方法:

  • 从这个信号量中获取一个许可,阻塞直到一个许可可用或者调用线程被中断。InterruptedException中断时抛出。
  • void acquire(int permits):从这个信号量中获取permits许可,阻塞直到它们可用或者调用线程被中断。InterruptedException被打断时抛出;当permits小于零时IllegalArgumentException被抛出。
  • 获得许可证,直到有一个可用的为止。
  • void acquireUninterruptibly(int permits):获取permits许可,阻塞直至全部可用。当permits小于零时IllegalArgumentException被抛出。
  • int availablePermits():返回当前可用许可证的数量。此方法对于调试和测试非常有用。
  • 获取并返回所有立即可用的许可证的数量。
  • int getQueueLength():返回等待获取许可的线程数量的估计值。返回值只是一个估计值,因为在此方法遍历内部数据结构时,线程的数量可能会动态变化。该方法设计用于监控系统状态,而不是用于同步控制。
  • boolean hasQueuedThreads():查询是否有线程正在等待获取许可。因为取消可能随时发生,所以一个true返回值并不能保证另一个线程会获得许可。这种方法主要用于监控系统状态。当可能有其他等待线程时,它返回true
  • boolean isFair():返回公平性设置(true表示公平,false表示不公平)。
  • void release():释放一个许可,将其返回给信号量。可用许可证的数量增加一个。如果有任何线程试图获取一个许可,则选择一个线程并给予它刚刚释放的许可。出于线程调度的目的,该线程被重新启用。
  • void release(int permits):释放permits许可,将它们返回给信号量。可用许可证的数量因permits而增加。如果有任何线程试图获取许可,就会选择一个线程,并给它刚刚释放的许可。如果可用许可的数量满足该线程的请求,则为了线程调度的目的,该线程被重新启用;否则,线程将等待,直到有足够的许可可用。如果在这个线程的请求被满足后还有可用的许可,那么这些许可被分配给试图获取许可的其他线程。当permits小于零时IllegalArgumentException被抛出。
  • String toString():返回一个标识这个信号量及其状态的字符串。括号中的状态包括字符串文字"Permits =",后跟许可数量。
  • 从这个信号量获取一个许可,但是只有在调用时有一个许可可用的时候。获得许可时返回true。否则,立即返回值false
  • boolean tryAcquire(int permits):从这个信号量中获取permits许可,但是只有当它们在调用时可用时。获得许可后返回true。否则,立即返回值false。当permits小于零时IllegalArgumentException被抛出。
  • boolean tryAcquire(int permits, long timeout, TimeUnit unit):类似于前面的方法,但是当permits许可不可用时,调用线程等待。当许可变得可用、超时过期或调用线程被中断时,等待结束,在这种情况下会抛出InterruptedException
  • boolean tryAcquire(long timeOut, TimeUnit unit):类似于tryAcquire(int permits),但是调用线程等待,直到许可可用。当许可变得可用、超时过期或调用线程被中断时,等待结束,在这种情况下会抛出InterruptedException

清单 6-4 扩展了Semaphore的 Javadoc 中呈现的“控制对项目池的访问”Semaphore示例。

Listing 6-4. Using a Counting Semaphore to Control Access to a Pool of Items

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo

{

public static void main(String[] args)

{

final Pool pool = new Pool();

Runnable r = new Runnable()

{

@Override

public void run()

{

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

try

{

while (true)

{

String item;

System.out.println(name + " acquiring " +

(item = pool.getItem()));

Thread.sleep(200 +

(int) (Math.random() * 100));

System.out.println(name + " putting back " +

item);

pool.putItem(item);

}

}

catch (InterruptedException ie)

{

System.out.println(name + "interrupted");

}

}

};

ExecutorService[] executors =

new ExecutorService[Pool.MAX_AVAILABLE + 1];

for (int i = 0; i < executors.length; i++)

{

executors[i] = Executors.newSingleThreadExecutor();

executors[i].execute(r);

}

}

}

final class Pool

{

public static final int MAX_AVAILABLE = 10;

private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);

private final String[] items;

private final boolean[] used = new boolean[MAX_AVAILABLE];

Pool()

{

items = new String[MAX_AVAILABLE];

for (int i = 0; i < items.length; i++)

items[i] = "I" + i;

}

String getItem() throws InterruptedException

{

available.acquire();

return getNextAvailableItem();

}

void putItem(String item)

{

if (markAsUnused(item))

available.release();

}

private synchronized String getNextAvailableItem()

{

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

{

if (!used[i])

{

used[i] = true;

return items[i];

}

}

return null; // not reached

}

private synchronized boolean markAsUnused(String item)

{

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

{

if (item == items[i])

{

if (used[i])

{

used[i] = false;

return true;

}

else

return false;

}

}

return false;

}

}

清单 6-4 的默认主线程创建了一个资源池,一个用于重复获取和放回资源的 runnable,以及一个执行器数组。每个执行者被告知执行可运行的。

PoolString getItem()void putItem(String item)方法获取并返回基于字符串的资源。在获得getItem()中的一个项目之前,调用线程必须从信号量中获得一个许可,这保证了一个项目可供使用。当线程处理完该项时,它调用putItem(String),后者将该项返回到池中,然后向信号量释放一个许可,让另一个线程获取该项。

当调用acquire()时,不持有同步锁,因为这将阻止一个项目返回到池中。但是,String getNextAvailableItem()boolean markAsUnused(String item)是同步的,以保持池的一致性。(信号量封装了用于限制对池的访问的同步,这与维护池一致性所需的同步是分开的。)

编译清单 6-4 如下:

javac SemaphoreDemo.java

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

java SemaphoreDemo

您应该观察到类似如下的输出前缀(消息顺序可能有所不同):

pool-1-thread-1 acquiring I0

pool-2-thread-1 acquiring I1

pool-3-thread-1 acquiring I2

pool-5-thread-1 acquiring I3

pool-7-thread-1 acquiring I4

pool-4-thread-1 acquiring I5

pool-6-thread-1 acquiring I6

pool-9-thread-1 acquiring I7

pool-8-thread-1 acquiring I8

pool-10-thread-1 acquiring I9

pool-9-thread-1 putting back I7

pool-2-thread-1 putting back I1

pool-11-thread-1 acquiring I7

pool-9-thread-1 acquiring I1

pool-8-thread-1 putting back I8

pool-2-thread-1 acquiring I8

pool-5-thread-1 putting back I3

pool-8-thread-1 acquiring I3

pool-4-thread-1 putting back I5

pool-5-thread-1 acquiring I5

pool-6-thread-1 putting back I6

pool-4-thread-1 acquiring I6

pool-1-thread-1 putting back I0

pool-6-thread-1 acquiring I0

pool-7-thread-1 putting back I4

pool-1-thread-1 acquiring I4

pool-10-thread-1 putting back I9

pool-7-thread-1 acquiring I9

pool-3-thread-1 putting back I2

pool-10-thread-1 acquiring I2

短语

相位器是一种更灵活的循环屏障。像一个循环屏障一样,phaser 让一组线程等待一个屏障;这些线程在最后一个线程到达后继续。相位器也提供了相当于屏障的作用。与协调固定数量线程的循环屏障不同,phaser 可以协调可变数量的线程,这些线程可以在任何时候注册。为了实现这种能力,相位器使用相位和相位号。

相位是相位器的当前状态,该状态由基于整数的相位号标识。当最后一个注册的线程到达相位器屏障时,相位器前进到下一个相位,并将其相位号加 1。

java.util.concurrent.Phaser类实现了相位器。因为这个类在其 Javadoc 中得到了充分的描述,所以我将只指出几个构造函数和方法:

  • Phaser(int threads)构造函数创建一个 phaser,它最初协调nthreads线程(这些线程还没有到达 phaser barrier ),其相位号最初设置为 0。
  • int register()方法向 phaser 添加一个新的未驱动线程,并返回阶段号来对到达进行分类。这个数字被称为到达阶段数。
  • int arriveAndAwaitAdvance()方法记录到达并等待 phaser 前进(这发生在其他线程到达之后)。它返回到达应用的阶段号。
  • int arriveAndDeregister()方法到达这个 phaser 并取消注册,而不等待其他 phaser 到达,从而减少了在未来阶段前进所需的线程数量。

清单 6-5 展示了相位器同步器。它基于Phaser的 Javadoc 中的第一个例子。

Listing 6-5. Using a Phaser to Control a One-Shot Action Serving a Variable Number of Parties

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Executors;

import java.util.concurrent.Phaser;

public class PhaserDemo

{

public static void main(String[] args)

{

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

tasks.add(() -> System.out.printf("%s running at %d%n",

Thread.currentThread().getName(),

System.currentTimeMillis()));

tasks.add(() -> System.out.printf("%s running at %d%n",

Thread.currentThread().getName(),

System.currentTimeMillis()));

runTasks(tasks);

}

static void runTasks(List<Runnable> tasks)

{

final Phaser phaser = new Phaser(1); // "1" (register self)

// create and start threads

for (final Runnable task: tasks)

{

phaser.register();

Runnable r = () ->

{

try

{

Thread.sleep(50 + (int) (Math.random() * 300));

}

catch (InterruptedException ie)

{

System.out.println("interrupted thread");

}

phaser.arriveAndAwaitAdvance(); // await the ...

// creation of ...

// all tasks

task.run();

};

Executors.newSingleThreadExecutor().execute(r);

}

// allow threads to start and deregister self

phaser.arriveAndDeregister();

}

}

清单 6-5 的默认主线程创建了一对可运行的任务,每个任务报告它开始运行的时间(以毫秒为单位)。然后,在创建了一个Phaser实例并等待两个任务到达关卡之后,运行这些任务。

编译清单 6-5 如下:

javac PhaserDemo.java

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

java PhaserDemo

您应该观察到类似于以下内容的输出(应用不应该结束—按 Ctrl+C 或您的等效按键来结束应用):

pool-1-thread-1 running at 1445806012709

pool-2-thread-1 running at 1445806012712

正如您从倒计时锁行为中所预期的那样,两个线程同时开始运行(在本例中),尽管由于Thread.sleep()的存在,一个线程可能已经延迟了 349 毫秒。

注释掉phaser.arriveAndAwaitAdvance(); // await the ...,您现在应该观察到线程在完全不同的时间开始,如下所示:

pool-2-thread-1 running at 1445806212870

pool-1-thread-1 running at 1445806213013

Exercises

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

Define synchronizer.   Describe the behavior of a countdown latch.   What happens when CountDownLatch’s void countDown() method is called and the count reaches zero?   Describe the behavior of a cyclic barrier.   True or false: CyclicBarrier’s int await() method returns -1 when the barrier is reset while any thread is waiting or when the barrier is broken when await() is invoked.   Describe the behavior of an exchanger.   What does Exchanger’s V exchange(V x) method accomplish?   Describe the behavior of a semaphore.   Identify the two kinds of semaphores.   Describe the behavior of a phaser.   What does Phaser’s int register() method return?   Listing 3-2 (in Chapter 3) presented an enhanced PC application. Recreate this application where the synchronization is handled by the Semaphore class.  

摘要

Java 提供了synchronized关键字来同步线程对临界区的访问。因为很难正确编写基于synchronized的同步代码,所以在并发工具中包含了高级同步器。

倒计时锁存导致一个或多个线程在“门”处等待,直到另一个线程打开这个门,此时这些其他线程可以继续。它由一个计数和“使线程等待,直到计数达到零”以及“递减计数”的操作组成

循环障碍让一组线程相互等待到达一个公共障碍点。屏障是循环的,因为它可以在等待线程被释放后被重用。这种同步器在应用中非常有用,这些应用包含一组固定大小的线程,它们偶尔会相互等待。

交换器提供了一个同步点,线程可以在这里交换对象。每个线程在交换器的exchange()方法的入口提供一些对象,与一个伙伴线程匹配,并在返回时接收其伙伴的对象。

信号量维护一组许可证,用于限制可以访问有限资源的线程数量。当没有许可可用时,试图获取许可的线程将被阻塞,直到某个其他线程释放许可。

相位器是一种更灵活的循环屏障。像一个循环屏障一样,phaser 让一组线程等待一个屏障;这些线程在最后一个线程到达后继续。相位器也提供了相当于屏障的作用。与协调固定数量线程的循环屏障不同,phaser 可以协调可变数量的线程,这些线程可以在任何时候注册。为了实现这种能力,相位器使用相位和相位号。

第七章介绍了锁定框架。

七、锁定框架

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

java.util.concurrent.locks包提供了一个接口和类的框架,用于锁定和等待条件,其方式不同于对象固有的基于锁的同步和java.lang.Object的等待/通知机制。并发工具包括锁定框架,该框架通过提供锁轮询、定时等待等改进了内在同步和等待/通知。

Synchronized and Low-Level Locking

Java 支持同步,因此线程可以安全地更新共享变量,并确保一个线程的更新对其他线程可见。通过用synchronized关键字标记方法或代码块,可以在代码中利用同步。这些代码序列被称为临界区。Java 虚拟机(JVM)通过监视器和monitorentermonitorexit JVM 指令支持同步。

每个 Java 对象都与一个监视器相关联,这是一个互斥(一次只让一个线程在一个临界区中执行)结构,它可以防止多个线程同时在一个临界区中执行。在线程进入临界区之前,需要锁定监视器。如果监视器已经被锁定,线程将一直阻塞,直到监视器被解锁(通过另一个线程离开临界区)。

当线程在多核/多处理器环境中锁定监视器时,存储在主存储器中的共享变量的值被读入存储在线程的工作存储器(也称为本地存储器或高速缓冲存储器)中的这些变量的副本中。此操作确保线程将使用这些变量的最新值,而不是过时的值,这就是所谓的可见性。线程继续处理这些共享变量的副本。当线程在离开临界区时解锁监视器,其共享变量副本中的值被写回主存,这使得下一个进入临界区的线程可以访问这些变量的最新值。(volatile关键字只处理可见性。)

锁定框架包括常用的LockReentrantLockConditionReadWriteLockReentrantReadWriteLock类型,我将在本章中探讨这些类型。我也简单介绍一下StampedLock类,是 Java 8 引入的。

Lock接口提供了比监视器相关锁更广泛的锁定操作。例如,当锁不可用时,您可以立即退出获取锁的尝试。该接口声明了以下方法:

  • void lock():获取锁。当锁不可用时,调用线程被迫等待,直到锁可用。
  • void lockInterruptibly():除非调用线程中断,否则获取锁。当锁不可用时,调用线程被迫等待,直到锁可用或线程被中断,这导致该方法抛出java.lang.InterruptedException
  • Condition newCondition():返回一个绑定到这个Lock实例的新的Condition实例。当Lock实现类不支持条件时,该方法抛出java.lang.UnsupportedOperationException
  • boolean tryLock():在调用该方法时,获取可用的锁。当获得锁时,该方法返回true,当没有获得锁时,该方法返回false
  • boolean tryLock(long time, TimeUnit unit):当锁在指定的等待time内可用时,获取锁,以unit java.util.concurrent.TimeUnit为单位(秒、毫秒等),并且调用线程没有被中断。当锁不可用时,调用线程被迫等待,直到它在等待时间内变得可用,或者线程被中断,这导致该方法抛出InterruptedException。获得锁时,true返回;否则,false返回。
  • void unlock():解除锁定。

获得的锁必须被释放。在同步方法和块以及与每个对象关联的隐式监视器锁的上下文中,所有锁的获取和释放都以块结构的方式发生。当获得多个锁时,它们以相反的顺序被释放,并且所有的锁都在它们被获得的同一个词法范围内被释放。

锁的获取和释放在Lock接口实现的上下文中可以更加灵活。例如,一些用于遍历并发访问的数据结构的算法需要使用“移交”或“链式锁定”:您获取节点 A 的锁,然后获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,以此类推。通过允许在不同的范围内获取和释放锁,以及允许以任何顺序获取和释放多个锁,Lock接口的实现支持这种技术的使用。

随着灵活性的增加,责任也随之增加。块结构锁定的缺失消除了同步方法和块中发生的锁的自动释放。因此,对于锁的获取和释放,您通常应该采用以下习惯用法:

Lock l = ...; // ... is a placeholder for code that obtains the lock

l.lock();

try

{

// access the resource protected by this lock

}

catch (Exception ex)

{

// restore invariants

}

finally

{

l.unlock();

}

这个习惯用法确保获得的锁总是被释放。

Note

所有的Lock实现都需要执行与内置监控锁提供的相同的内存同步语义。

可重入锁

Lock是由ReentrantLock类实现的,它描述了一个可重入的互斥锁。此锁与保持计数相关联。当线程持有锁并通过调用lock()lockUninterruptibly()tryLock()方法之一重新获取锁时,持有计数增加 1。当线程调用unlock()时,保持计数减 1。当该计数达到 0 时,锁被释放。

ReentrantLock提供了与隐式监控锁相同的并发性和内存语义,通过同步的方法和块来访问。但是,它具有扩展的功能,并在高线程争用情况下(线程频繁请求获取另一个线程已经持有的锁)提供更好的性能。当许多线程试图访问一个共享资源时,JVM 花更少的时间调度这些线程,花更多的时间执行它们。

通过调用以下任一构造函数来初始化ReentrantLock实例:

  • ReentrantLock():创建一个ReentrantLock的实例。这个构造函数相当于ReentrantLock(false)
  • ReentrantLock(boolean fair):用指定的公平策略创建一个ReentrantLock的实例。当这个锁应该使用公平排序策略时,将true传递给fair:在争用的情况下,锁会优先授予等待时间最长的线程访问权。

ReentrantLock实现Lock的方法。然而,当调用线程没有持有锁时,unlock()的实现抛出java.lang.IllegalMonitorStateException。同样,ReentrantLock提供了自己的方法。例如,当锁被当前线程持有时,boolean isFair()返回公平策略,boolean isHeldByCurrentThread()返回true。清单 7-1 演示了Reentrant Lock

Listing 7-1. Achieving Synchronization in Terms of Reentrant Locks

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;

public class RLDemo

{

public static void main(String[] args)

{

ExecutorService executor = Executors.newFixedThreadPool(2);

final ReentrantLock lock = new ReentrantLock();

class Worker implements Runnable

{

private final String name;

Worker(String name)

{

this.name = name;

}

@Override

public void run()

{

lock.lock();

try

{

if (lock.isHeldByCurrentThread())

System.out.printf("Thread %s entered critical section.%n",name);

System.out.printf("Thread %s performing work.%n", name);

try

{

Thread.sleep(2000);

}

catch (InterruptedException ie)

{

ie.printStackTrace();

}

System.out.printf("Thread %s finished working.%n", name);

}

finally

{

lock.unlock();

}

}

}

executor.execute(new Worker("ThdA"));

executor.execute(new Worker("ThdB"));

try

{

executor.awaitTermination(5, TimeUnit.SECONDS);

}

catch (InterruptedException ie)

{

ie.printStackTrace();

}

executor.shutdownNow();

}

}

清单 7-1 描述了一个应用,它的默认主线程创建了一对工作线程,它们进入、模拟工作和离开临界区。他们使用ReentrantLocklock()unlock()方法来获得和释放可重入锁。当线程调用lock()并且锁不可用时,线程被禁用(并且不能被调度),直到锁变得可用。

编译清单 7-1 如下:

javac RLDemo.java

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

java RLDemo

您应该会发现类似于以下内容的输出(消息顺序可能会有所不同):

Thread ThdA entered critical section.

Thread ThdA performing work.

Thread ThdA finished working.

Thread ThdB entered critical section.

Thread ThdB performing work.

Thread ThdB finished working.

情况

Condition接口将Object的等待和通知方法(wait()notify()notifyAll())分解成不同的条件对象,通过将它们与任意的Lock实现结合起来,给出每个对象有多个等待集的效果。其中Lock替换synchronized方法和块,Condition替换Object的等待/通知方法。

Note

一个Condition实例本质上绑定到一个锁。为了获得某个Lock实例的Condition实例,使用LocknewCondition方法。

Condition声明以下方法:

  • void await():强制调用线程等待,直到它收到信号或被中断。
  • boolean await(long time, TimeUnit unit):强制调用线程等待,直到它被信号通知或中断,或者直到指定的等待时间过去。
  • long awaitNanos(long nanosTimeout):强制当前线程等待,直到它发出信号或被中断,或者直到指定的等待时间过去。
  • void awaitUninterruptibly():强制当前线程等待,直到它收到信号。
  • boolean awaitUntil(Date deadline):强制当前线程等待,直到它发出信号或被中断,或者直到指定的deadline过去。
  • void signal():唤醒一个等待线程。
  • void signalAll():唤醒所有等待的线程。

清单 7-2 重温了第三章的生产者-消费者应用(在清单 3-2 中)向您展示如何编写它来利用条件。

Listing 7-2. Achieving Synchronization in Terms of Locks and Conditions

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class PC

{

public static void main(String[] args)

{

Shared s = new Shared();

new Producer(s).start();

new Consumer(s).start();

}

}

class Shared

{

private char c;

private volatile boolean available;

private final Lock lock;

private final Condition condition;

Shared()

{

available = false;

lock = new ReentrantLock();

condition = lock.newCondition();

}

Lock getLock()

{

return lock;

}

char getSharedChar()

{

lock.lock();

try

{

while (!available)

try

{

condition.await();

}

catch (InterruptedException ie)

{

ie.printStackTrace();

}

available = false;

condition.signal();

}

finally

{

lock.unlock();

return c;

}

}

void setSharedChar(char c)

{

lock.lock();

try

{

while (available)

try

{

condition.await();

}

catch (InterruptedException ie)

{

ie.printStackTrace();

}

this.c = c;

available = true;

condition.signal();

}

finally

{

lock.unlock();

}

}

}

class Producer extends Thread

{

private final Lock l;

private final Shared s;

Producer(Shared s)

{

this.s = s;

l = s.getLock();

}

@Override

public void run()

{

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

{

l.lock();

s.setSharedChar(ch);

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

l.unlock();

}

}

}

class Consumer extends Thread

{

private final Lock l;

private final Shared s;

Consumer(Shared s)

{

this.s = s;

l = s.getLock();

}

@Override

public void run()

{

char ch;

do

{

l.lock();

ch = s.getSharedChar();

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

l.unlock();

}

while (ch != 'Z');

}

}

清单 7-2 类似于清单 3-2 的PC应用。但是,它用锁和条件代替了synchronized和等待/通知。

PCmain()方法实例化了SharedProducerConsumer类。将Shared实例传递给ProducerConsumer构造函数,然后启动这些线程。

在默认的主线程上调用ProducerConsumer构造函数。因为Shared实例也被生产者和消费者线程访问,所以这个实例必须对这些线程可见(特别是当这些线程运行在不同的内核上时)。在ProducerConsumer中,我通过声明sfinal来完成这个任务。我本可以将这个字段声明为volatile,但是volatile建议对该字段进行额外的写入,并且s在初始化后不应该被更改。

查看Shared的构造函数。注意,它通过lock = new ReentrantLock();创建了一个锁,并通过condition = lock.newCondition() ;创建了一个与这个锁相关联的条件。这个锁通过Lock getLock()方法对生产者线程和消费者线程可用。

生产者线程调用Sharedvoid setSharedChar(char c)方法来生成一个新的字符,然后输出一个标识所生成字符的消息。这个方法锁定之前创建的Lock对象,并进入一个while循环,重复测试变量available,当一个制作好的角色可供消费时就是true

availabletrue时,生产者调用条件的await()方法等待available变为false。当生产者消耗完角色时,消费者发出条件信号以唤醒生产者。(我使用循环而不是if语句,因为虚假唤醒是可能的,并且available可能仍然是true。)

离开循环后,生产者线程记录新字符,将true分配给available以指示新字符可供消费,并发出条件信号以唤醒等待的消费者。最后,它打开锁,退出setSharedChar()

Note

我在Producerrun()方法中锁定了setSharedChar() / System.out.println()块,在Consumerrun()方法中锁定了getSharedChar() / System.out.println()块,以防止应用在生成消息之前输出消费消息,即使字符是在消费之前生成的。

消费者线程和getSharedChar()方法的行为类似于我刚刚描述的生产者线程和setSharedChar()方法。

Note

我没有使用try / finally习惯用法来确保在ProducerConsumerrun()方法中释放锁,因为在这个上下文中不会抛出异常。

编译清单 7-2 如下:

javac PC.java

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

java PC

您应该观察到与输出的以下前缀相同的输出,这表示锁步同步(生产者线程直到项被消费后才产生项,消费者线程直到项被产生后才消费项):

A produced by producer.

A consumed by consumer.

B produced by producer.

B consumed by consumer.

C produced by producer.

C consumed by consumer.

D produced by producer.

D consumed by consumer.

读写锁

数据结构被读取的次数比被修改的次数多。例如,您可能已经创建了一个单词定义的在线词典,许多线程将同时读取该词典,而单个线程可能偶尔会添加新的定义或更新现有的定义。锁定框架为这些情况提供了读写锁定机制,在读取时产生更大的并发性,在写入时产生独占访问的安全性。这种机制基于ReadWriteLock接口。

维护一对锁:一个锁用于只读操作,另一个锁用于写操作。只要没有写线程,读锁就可以被多个读线程同时持有。写锁是排他的:只有一个线程可以修改共享数据。(与关键字synchronized相关联的锁也是排他的。)

ReadWriteLock声明以下方法:

  • Lock readLock():归还用于阅读的锁。
  • Lock writeLock():归还用于书写的锁。

可重入读写锁

ReadWriteLock是由ReentrantReadWriteLock类实现的,它描述了一个可重入的读写锁,其语义与ReentrantLock相似。

通过调用以下任一构造函数来初始化ReentrantReadWriteLock实例:

  • ReentrantReadWriteLock():创建一个ReentrantReadWriteLock的实例。这个构造函数相当于ReentrantReadWriteLock(false)
  • ReentrantReadWriteLock(boolean fair):用指定的公平策略创建一个ReentrantReadWriteLock的实例。当这个锁应该使用公平排序策略时,将true传递给fair

Note

对于公平排序策略,当当前持有的锁被释放时,等待时间最长的单个写线程将被分配写锁,或者当有一组读线程等待的时间比所有等待的写线程长时,该组将被分配读锁。

当写锁被持有或者有一个等待的写线程时,试图获得公平读锁(不可重入)的线程将会阻塞。在当前等待的最早的写线程获得并释放写锁之前,该线程不会获得读锁。如果一个等待的写线程放弃了它的等待,留下一个或多个读线程作为队列中等待时间最长的写线程,这些读线程将被分配读锁。

试图获得公平写锁(不可重入)的线程将会阻塞,除非读锁和写锁都是自由的(这意味着没有等待线程)。(非阻塞tryLock ()方法不遵守这个公平设置,如果可能的话会立即获取锁,而不管等待的线程。)

实例化该类后,调用以下方法来获取读写锁:

  • ReentrantReadWriteLock.ReadLock readLock():返回用于读取的锁。
  • ReentrantReadWriteLock.WriteLock writeLock():返回用于写入的锁。

每个嵌套的ReadLockWriteLock类都实现了Lock接口并声明了自己的方法。此外,ReentrantReadWriteLock声明了额外的方法,如下面的一对:

  • int getReadHoldCount():返回调用线程对这个锁的可重入读持有的次数,当读锁没有被调用线程持有时为0。对于每个与解锁操作不匹配的锁定操作,读取器线程都持有一个锁。
  • int getWriteHoldCount():返回调用线程对这个锁的可重入写持有的次数,当写锁没有被调用线程持有时为0。对于每个与解锁操作不匹配的锁定操作,写线程都持有一个锁。

为了演示ReadWriteLockReentrantReadWriteLock,清单 7-3 给出了一个应用,它的写线程填充一个单词/定义条目的字典,而读线程连续地随机访问条目并输出它们。

Listing 7-3. Using ReadWriteLock to Satisfy a Dictionary Application’s Reader and Writer Threads

import java.util.HashMap;

import java.util.Map;

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Dictionary

{

public static void main(String[] args)

{

final String[] words =

{

"hypocalcemia",

"prolixity",

"assiduous",

"indefatigable",

"castellan"

};

final String[] definitions =

{

"a deficiency of calcium in the blood",

"unduly prolonged or drawn out",

"showing great care, attention, and effort",

"able to work or continue for a lengthy time without tiring",

"the govenor or warden of a castle or fort"

};

final Map<String, String> dictionary = new HashMap<String, String>();

ReadWriteLock rwl = new ReentrantReadWriteLock(true);

final Lock rlock = rwl.readLock();

final Lock wlock = rwl.writeLock();

Runnable writer = () ->

{

for (int i = 0; i < words.length; i++)

{

wlock.lock();

try

{

dictionary.put(words[i],

definitions[i]);

System.out.println("writer storing " +

words[i] + " entry");

}

finally

{

wlock.unlock();

}

try

{

Thread.sleep(1);

}

catch (InterruptedException ie)

{

System.err.println("writer " +

"interrupted");

}

}

};

ExecutorService es = Executors.newFixedThreadPool(1);

es.submit(writer);

Runnable reader = () ->

{

while (true)

{

rlock.lock();

try

{

int i = (int) (Math.random() *

words.length);

System.out.println("reader accessing " +

words[i] + ": " +

dictionary.get(words[i])

+ " entry");

}

finally

{

rlock.unlock();

}

}

};

es = Executors.newFixedThreadPool(1);

es.submit(reader);

}

}

清单 7-3 的默认主线程首先创建字符串的wordsdefinitions数组,它们被声明为final,因为它们将被匿名类访问。在创建了存储单词/定义条目的映射之后,它获得一个可重入的读/写锁,并访问读/写锁。

现在为编写器线程创建了一个 runnable。它的run()方法迭代words数组。每次迭代都会锁定写线程锁。当此方法返回时,编写器线程拥有独占的编写器锁,并且可以更新映射。它通过调用地图的put()方法来实现。在输出一条消息来标识添加的单词后,编写器线程释放锁并休眠一毫秒,以表现出正在执行其他工作。基于线程池的执行器被获取并用于调用写线程的 runnable。

随后为读取器线程创建一个 runnable。它的run()方法反复获取读锁,访问 map 中的一个随机条目,输出这个条目,解锁读锁。获得一个基于线程池的执行器,并用于调用读取器线程的 runnable。

虽然我本可以避免使用获取和释放锁的习惯用法,因为不会抛出异常,但我还是指定了try / finally作为良好的形式。

编译清单 7-3 如下:

javac Dictionary.java

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

java Dictionary

您应该观察到类似于我在一次执行中观察到的输出的以下前缀的输出(消息顺序可能有些不同):

writer storing hypocalcemia entry

writer storing prolixity entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

writer storing assiduous entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing castellan: null entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing indefatigable: null entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing indefatigable: null entry

reader accessing prolixity: unduly prolonged or drawn out entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing castellan: null entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing prolixity: unduly prolonged or drawn out entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing castellan: null entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

reader accessing indefatigable: null entry

reader accessing castellan: null entry

reader accessing prolixity: unduly prolonged or drawn out entry

reader accessing hypocalcemia: a deficiency of calcium in the blood entry

writer storing indefatigable entry

reader accessing assiduous: showing great care, attention, and effort entry

reader accessing assiduous: showing great care, attention, and effort entry

Note

Java 8 在java.util.concurrent.locks包中增加了StampedLock。根据其 JDK 8 文档,StampedLock是一种基于能力的锁,具有三种模式来控制读/写访问。它以类似于ReentrantReadWriteLock的方式区分排他锁和非排他锁,但也允许乐观读取,这是ReentrantReadWriteLock不支持的。查看 Heinz Kabutz 博士的 Phaser 和 StampedLock 并发同步器视频演示( www.parleys.com/tutorial/5148922b0364bc17fc56ca4f/chapter0/about )以了解StampedLock。另外,请参见本演示文稿的 PDF 文件( www.jfokus.se/jfokus13/preso/jf13_PhaserAndStampedLock.pdf )。

Exercises

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

Define lock.   What is 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)?   True or false: ReentrantLock’s unlock() method throws IllegalMonitorStateException when the calling thread doesn’t hold the lock.   How do you obtain a Condition instance for use with a particular Lock instance?   True or false: ReentrantReadWriteLock() creates an instance of ReentrantReadWriteLock with a fair ordering policy.   Define StampedLock.   The java.util.concurrent.locks package includes a LockSupport class. What is the purpose of LockSupport?   Replace the following ID class with an equivalent class that uses ReentrantLock in place of synchronized:  

public class ID

{

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

public static synchronized int getID()

{

int temp = counter + 1;

try

{

Thread.sleep(1);

}

catch (InterruptedException ie)

{

}

return counter = temp;

}

}

摘要

java.util.concurrent.locks包提供了一个接口和类的框架,用于锁定和等待条件,其方式不同于对象固有的基于锁的同步和Object的等待/通知机制。并发工具包括一个锁定框架,该框架通过提供锁轮询、定时等待等改进了内在同步和等待/通知。

锁定框架包括常用的LockReentrantLockConditionReadWriteLockReentrantReadWriteLock类型,我在本章中对此进行了探讨。我也简单介绍了一下StampedLock类,它是 Java 8 中引入的。

第八章介绍了额外的并发工具。