目录
并发 —— Java平台有api来帮助您开发多线程程序
计算机用户理所当然地认为他们的系统一次可以做不止一件事。他们假设它们可以继续在文字处理器中工作,而其他应用程序则下载文件、管理打印队列和流式传输音频。即使是单个应用程序也经常被期望一次做不止一件事。例如,流式音频应用程序必须同时从网络读取数字音频、解压缩、管理播放并更新其显示。即使是文字处理器也应该随时准备好响应键盘和鼠标事件,无论它多么忙于重新格式化文本或更新显示。可以做这些事情的软件被称为并发软件。
Java平台从头开始设计,以支持并发编程,并在Java编程语言和Java类库中提供基本的并发支持。从 5.0 版开始,Java 平台还包含高级并发 API。本课介绍了平台的基本并发支持,并总结了java.util.concurrent包中的一些高级api。
进程和线程
在并发编程中,有两个基本的执行单元:进程和线程。在Java编程语言中,并发编程主要关注线程。然而,进程也很重要。
计算机系统通常具有许多活跃的进程和线程。即使在单核的系统中也是如此,因此在任何给定的时刻都只有一个线程在实际执行。单核的处理时间通过一个被称为时间切片的操作系统特性在进程和线程之间共享
具有多个处理器或具有多个执行核心的处理器的计算机系统变得越来越普遍。这极大地增强了系统并发执行进程和线程的能力——但是,即使在没有多个处理器或执行核心的简单系统上,并发也是可能的
进程
一个进程有一个独立的执行环境。一个进程通常有一个完整的、私有的基本运行时资源集;特别是,每个进程都有自己的内存空间。
进程通常被视为程序或应用程序的同义词。然而,用户所看到的单个应用程序实际上可能是一组协作进程。为了方便进程之间的通信,大多数操作系统都支持进程间通信Inter Process Communication(IPC)资源,比如管道和套接字。IPC 不仅用于同一系统上的进程之间的通信,还用于不同系统上的进程之间的通信。
Java虚拟机的大多数实现都是作为单个进程运行的。Java应用程序可以使用ProcessBuilder对象创建其他进程。多进程应用程序超出了本课的范围。
线程
线程有时被称为轻量级进程。但是创建一个新线程比创建一个新进程需要更少的资源。
线程存在于进程中——每个进程至少有一个。线程共享进程的资源,包括内存和打开的文件。这使得沟通更有效率,但也有潜在的问题。
多线程执行是 Java 平台的一个基本特性。如果你计算“系统线程”像做内存管理和信号处理的线程的话,会发现每个应用程序至少有一个线程——或者几个。但从应用程序程序员的角度来看,一开始只需要一个线程,称为主线程。这个线程能够创建额外的线程,我们将在下一节中进行演示。
线程对象
每个线程都与该类的一个实例相关联 Thread。使用Thread对象创建并发应用程序有两种基本策略。
- 要直接控制线程的创建和管理,只需Thread在应用程序每次需要启动异步任务时进行实例化。
- 要将线程管理从应用程序的其余部分抽象出来,请将应用程序的任务传递给执行程序。
本节介绍Thread对象的使用。executor与其他高级并发对象一起讨论。
定义和启动线程
创建Thread实例的应用程序必须提供将在该线程中运行的代码。有两种方法可以做到这一点:
-
实现Runnable接口。Runnable接口定义了一个单独的方法run,用于包含线程中执行的代码。Runnable对象被传递给Thread构造函数,就像在HelloRunnable例子中那样:
public class HelloRunnable implements Runnable { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new Thread(new HelloRunnable())).start(); } }
-
继承Thread类。Thread类本身实现了Runnable,尽管它的run方法什么也不做。应用程序可以继承Thread,提供自己的run实现,如HelloThread的例子:
public class HelloThread extends Thread { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new HelloThread()).start(); } }
注意,这两个例子都调用Thread.start()以启动新线程。
你应该使用哪一个?第一种用法使用了Runnable对象,这种用法更为通用,因为Runnable对象可以继承Thread以外的类。第二种用法在简单的应用程序中更容易使用,但受限于任务类必须是Thread的子类。本课重点介绍第一种方法,它将Runnable任务与执行该任务的Thread对象分离开来。这种方法不仅更灵活,而且适用于后面介绍的高级线程管理api。
Thread类定义了许多用于线程管理的方法。这些方法包括静态方法,静态方法提供关于调用该方法的线程的信息或影响其状态的方法。其他方法是被其他的用于线程管理的线程和其他线程对象调用使用的。我们将在下面几节中研究其中的一些方法。
用sleep暂停线程
Thread.sleep导致当前线程在指定的时间内暂停执行。这是一种有效的方法,可以使应用程序的其他线程或可能在计算机系统上运行的其他应用程序使用处理器。sleep方法还可以用于调整节奏(如下面的示例所示),以及等待另一个具有时间要求的任务的线程(如后面一节中的SimpleThreads示例)。
提供了两种重载版本的sleep方法:一个将睡眠时间指定为毫秒,另一个将睡眠时间指定为纳秒。但是,不能保证这些睡眠时间是精确的,因为它们受到底层操作系统提供的工具的限制。此外,睡眠周期可以通过interrupts来终止,我们将在后面的部分中看到。在任何情况下,您都不能假设调用sleep会在指定的时间内恰好挂起线程。
SleepMessages示例使用sleep以4秒的间隔打印消息:
public class SleepMessages {
public static void main(String args[])
throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
for (int i = 0;
i < importantInfo.length;
i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}
注意,main方法抛出了InterruptedException异常。这是当sleep处于活动状态时,另一个线程中断当前线程时,sleep抛出的异常。由于这个应用程序没有定义另一个引起中断的线程,所以它不需要去捕获InterruptedException。
Interrupts(中断)
中断是对线程的指示,它应该停止正在做的事情并做其他事情。由程序员来决定线程如何准确地响应中断,但是线程终止是很常见的。这是本课所强调的用法。
线程通过调用thread对象上的interrupt来发送一个中断。为了使中断机制正常工作,被中断的线程必须支持自己的中断。
支持中断
线程如何支持自己的中断?这取决于它当前正在做什么。如果线程频繁地调用抛出InterruptedException的方法,则在捕获该异常后,它只需从run方法返回。例如,假设SleepMessages例子中的for循环是在线程的Runnable对象的run方法中。那么它可能会被修改如下以支持中断:
for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// We've been interrupted: no more messages.
return;
}
// Print a message
System.out.println(importantInfo[i]);
}
许多抛出InterruptedException的方法,比如sleep,被设计成取消当前操作,并在接收到中断时立即返回。
如果一个线程运行了很长时间而没有调用抛出InterruptedException的方法,该怎么办?那么它必须周期性地调用Thread.interrupted,如果接收到中断则返回true。例如:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// We've been interrupted: no more crunching.
return;
}
}
在这个简单的示例中,代码简单地测试中断,并在收到中断时退出线程。在更复杂的应用程序中,抛出InterruptedException可能更有意义:
if (Thread.interrupted()) {
throw new InterruptedException();
}
处理中断后的操作的代码集中在catch子句中。
中断状态标志
中断机制是使用一个称为中断状态的内部标志来实现的。调用Thread.interrupt会设置这个标志。当线程通过调用静态方法thread .interrupted来检查中断时,中断状态将被重置,默认是false。非静态的isInterrupted方法被一个线程用来查询另一个线程的中断状态,它不会改变中断状态标志。
按照惯例,任何抛出InterruptedException而退出的方法都会在退出时重置中断状态。然而,总是有可能中断状态会被另一个调用中断的线程再次立即设置。
Joins
join方法允许一个线程等待另一个线程的完成。如果t是一个线程正在执行的Thread对象,t.join()导致当前线程暂停执行,直到t的线程终止。join的重载方法允许程序员指定一个等待时间。然而,与sleep一样,join依赖于OS的计时,所以你不应该假设join会等待你指定的时间。
和sleep一样,join通过一个InterruptedException退出来响应中断。
SimpleThreads 示例
下面的示例汇集了本节的一些概念。SimpleThreads由两个线程组成。第一个是每个Java应用程序都有的主线程(main)。主线程从Runnable对象MessageLoop创建一个新线程,并等待它完成。如果MessageLoop线程花了很长时间才完成,主线程就会中断它。
messagelloop线程打印出一系列消息。如果在打印所有消息之前中断,messagelloop线程将打印一条消息并退出。
public class SimpleThreads {
// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[])
throws InterruptedException {
// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;
// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}
threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();
threadMessage("Waiting for MessageLoop thread to finish");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}
同步
线程主要通过共享涉及到的字段和对象引用字段的访问来进行通信。这种交流方式非常有效,但也可能导致两种错误:线程冲突和内存一致性错误。防止这些错误所需的工具是同步。
但是,同步可能会引入线程竞争,当两个或多个线程试图同时访问相同的资源时就会发生竞争,从而导致Java运行时执行一个或多个线程的速度变慢,甚至暂停它们的执行。 Starvation and livelock都是线程竞争的形式。有关更多信息,请参阅liveness(活跃性)一节。
本节涉及以下主题:
- 线程冲突:描述了当多个线程访问共享数据时如何引入错误。
- 内存一致性错误:描述共享内存视图不一致导致的错误。
- 同步方法:描述了一个简单的习惯用法,可以有效防止线程冲突和内存一致性错误。
- Implicit Locks and Synchronization:描述更通用的同步习惯用法,并描述如何基于严格实现的锁进行同步。
- 原子访问:讨论了不能被其他线程干扰的操作的一般概念。
线程冲突
考虑一个名为Counter的简单类
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter的设计是这样的:每次调用递增函数时将c加1,每次调用递减函数时将c减1。但是,如果一个Counter对象被多个线程引用,那么线程之间的冲突可能会阻止这种情况的发生。
当两个操作在不同的线程中运行,但对相同的数据进行操作时,就会发生冲突。这意味着这两个操作由多个步骤组成,并且步骤序列重叠。
对Counter实例的操作似乎不可能交错,因为c上的两个操作都是单一的、简单的语句。然而,即使是简单的语句也可以被虚拟机转换成多个步骤。我们将不检查虚拟机的具体步骤——只要知道一个表达式c++可以分解为三个步骤就足够了:
- 检索c的当前值
- 将检索到的值增加1
- 将递增后的值存储回c中
表达式c——可以以同样的方式分解,除了第二步是递减而不是递增。
假设线程A在同一时间调用递增,而线程B调用递减。如果c的初始值为0,它们的交错动作可能遵循以下顺序:
- Thread A:检索c
- Thread B:检索c
- Thread A:增加检索值;结果是1。
- Thread B:减检索值;结果是1。
- Thread A:将结果存储在c中;C现在是1。
- Thread B:将结果存储在c中;C现在是-1。
线程A的结果丢失,被线程B覆盖。这种特殊的交叉顺序只是一种可能性。在不同的情况下,可能是线程B的结果丢失了,或者根本没有错误。因为线程冲突bug是不可预测的,所以很难检测和修复。
内存一致性错误
当不同的线程对应该是相同的数据有不一致的视图时,内存一致性错误就会发生。内存一致性错误的原因很复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需要的只是一个避免它们的策略。
避免内存一致性错误的关键是理解happens-before关系。这种关系只是保证一个特定语句的内存写入对另一个特定语句是可见的。请考虑以下示例。假设定义并初始化了一个简单的int字段:int counter = 0;
counter字段在两个线程A和b之间共享。假设线程A增加counter。counter++;
然后,不久之后,线程B打印出counter;System.out.println(counter);
如果这两条语句在同一个线程中执行,那么可以安全地假设输出的值是“1”。但是,如果这两条语句在不同的线程中执行,输出的值很可能是“0”,因为不能保证线程A对counter的更改对线程B是可见的——除非程序员在这两条语句之间建立了happens-before关系。
有几个行为是happens-before关系。其中之一是同步,我们将在下面的部分中看到。
这有两种行为符合happens-before关系。
- 当语句调用Thread.start,每个与该语句有happens-before关系的语句也与新线程执行的每个语句有happens-before关系。导致创建新线程的代码的效果对新线程是可见的。
- 当一个线程终止并导致一个线程join到另一个线程以返回,那么被终止的线程执行的所有语句与成功join之后的所有语句都有happens-before关系。线程中代码的效果现在对于执行join的线程是可见的。
有关创建happens-before关系的操作列表,请参阅java.util.concurrent包的Summary页面。
同步方法
Java编程语言提供了两个基本的同步习惯用法:同步方法和同步语句。这两个用法中比较复杂的同步语句将在下一节中描述。本节介绍同步方法。
要使一个方法同步,只需将synchronized关键字添加到它的声明中:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
如果count是SynchronizedCounter的一个实例,那么使这些方法同步有两个效果:
- 首先,同一个对象上的两个同步方法调用不可能交错。当一个线程为一个对象执行一个同步方法时,所有为同一对象调用同步方法的其他线程暂停执行,直到第一个线程完成该对象。
- 其次,当一个同步方法退出时,它会自动与该对象的任何后续同步方法调用建立happens-before关系。这保证了对对象状态的更改对所有线程都是可见的。
注意,构造函数不能被同步——在构造函数中使用synchronized关键字会导致语法错误。同步构造函数没有意义,因为只有创建对象的线程在构造对象时才有权访问它。
警告:当构造一个将在线程之间共享的对象时,要非常小心,不要过早地“泄漏”对该对象的引用。假设您想要维护一个名为instances的List实例,其中包含类中的每一个实例。你可能会想在构造函数中添加下面这行代码:`instances.add(this);`但是,其他线程可以在对象构造完成之前使用实例访问该对象。
同步方法启用了一个简单的策略来防止线程冲突和内存一致性错误:如果一个对象对多个线程可见,那么对该对象变量的所有读或写都是通过同步方法完成的。(一个重要的例外:final字段不能在对象构造后修改,但可以在对象构造后通过非同步方法安全地读取)。这种策略是有效的,但可能会带来liveness方面的问题,我们将在本课后面看到。
内在锁和同步
同步是围绕一个称为内在锁或监视器锁的内部实体构建的。(API规范通常将此实体简单地称为“监视器”。)。内在锁在同步的两个方面都发挥作用:强制对对象状态进行独占访问并建立对可见性至关重要的happens-before关系。
每个对象都有一个与之相关的内在锁。每个对象都有一个与之相关的内在锁。按照惯例,需要对对象的字段进行排他和一致访问的线程必须在访问它们之前获取对象的内在锁,然后在访问完它们之后释放内在锁。线程从获得锁到释放锁的这段时间内,被称为拥有内在锁。只要一个线程拥有一个内在锁,其他线程就不能获得相同的锁。当另一个线程试图获取锁时,它将阻塞。当线程释放一个内在锁时,在该动作和随后获得的任何锁之间就会建立happens-before关系。
同步方法中的锁
当线程调用一个同步方法时,它会自动获取该方法对象的内在锁,并在该方法返回时释放锁。即使返回是由未捕获的异常引起的,也会释放锁。
您可能想知道调用静态同步方法时会发生什么,因为静态方法是与类而不是对象相关联的。在这种情况下,线程获得与类关联的Class对象的内在锁。因此,对类的静态字段的访问是由一个锁控制的,这个锁不同于任何类实例的锁。
同步语句
另一种创建同步代码的方法是使用同步语句。与同步方法不同,同步语句必须指定提供内部锁的对象:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在本例中,该addName方法需要同步对lastNameand的更改nameCount,但还需要避免同步调用其他对象的方法。(从同步代码中调用其他对象的方法可能会产生问题,在 Liveness一节中有描述。)如果没有同步语句,就必须有一个单独的、非同步的方法来实现调用namlist .add的唯一目的。
同步语句对于改善细粒度同步的并发性也很有用。例如,假设类MsLunch有两个实例字段c1和c2,它们从未一起使用。这些字段的所有更新都必须同步,但是没有理由阻止c1的更新与c2的更新交替进行——这样做会造成不必要的阻塞,从而减少并发性。我们没有使用同步方法,也没有使用与this相关的锁,而是创建了两个对象来提供锁。
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
使用这个用法时要特别小心。您必须绝对确定交叉访问受影响的字段确实是安全的。
可重入同步
回想一下,一个线程不能获得另一个线程拥有的锁。但是线程可以获取它已经拥有的锁。允许一个线程多次获得同一个锁可以实现重入同步。这描述了一种情况,同步代码直接或间接地调用同样包含同步代码的方法,并且两组代码使用相同的锁。如果没有可重入同步,同步代码将不得不采取许多额外的预防措施,以避免线程导致自己阻塞。
原子访问
在编程中,原子动作是指一次有效地发生的动作。原子的动作不能中途停止:它要么完全发生,要么根本不发生。原子操作的副作用在操作完成之前是不可见的。
我们已经看到,自增表达式(如c++)不描述原子动作。即使是非常简单的表达式也可以定义复杂的操作,这些操作可以分解为其他操作。然而,有些动作可以指定为原子的:
- 对于引用变量和大多数基本变量(除了long和double之外的所有类型),读写都是原子的。
- 对于所有声明为volatile的变量(包括long和double变量),读写都是原子性的。
原子操作不能交叉使用,因此可以在不用担心线程干扰的情况下使用它们。然而,这并不能消除所有同步原子操作的需要,因为内存一致性错误仍然是可能的存在的。使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写操作都会与后续对该变量的读操作建立happens-before关系。这意味着对volatile变量的更改对其他线程总是可见的。更重要的是,这还意味着当线程读取volatile变量时,它不仅会看到volatile的最新更改,还会看到导致更改的代码的副作用。
使用简单的原子变量访问比通过同步代码访问这些变量更有效,但程序员需要更加小心,以避免内存一致性错误。额外的努力是否值得取决于应用程序的大小和复杂性。
java.util.concurrent包中的一些类提供了不依赖于同步的原子方法。我们将在高级并发对象一节中讨论它们。
活性
并发应用程序及时执行的能力称为其活动能力。本节描述最常见的一种活性问题——死锁,然后简要描述另外两个活性问题——饥饿和活锁。
死锁
死锁描述的是两个或多个线程永远被阻塞,等待对方的情况。这有个例子。
Alphonse 和 Gaston是朋友,都很懂得礼貌。礼貌的规矩是,当你向朋友鞠躬时,你必须一直鞠躬,直到你的朋友有机会回礼。不幸的是,这条规则并没有考虑到两个朋友同时向对方鞠躬的可能性。这个例子应用程序死锁,模拟了这种可能性:
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s"
+ " has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s"
+ " has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new Runnable() {
public void run() { alphonse.bow(gaston); }
}).start();
new Thread(new Runnable() {
public void run() { gaston.bow(alphonse); }
}).start();
}
}
当死锁运行时,极有可能两个线程在尝试调用bowBack时都阻塞。两个block都不会结束,因为每个线程都在等待另一个退出bow。
饥饿和活锁
饥饿和活锁比死锁更不常见,但仍然是每个并发软件设计师都可能遇到的问题。
饥饿
饥饿描述了一种情况,即线程无法获得对共享资源的常规访问,从而无法取得进展。当“贪婪的”线程使共享资源长时间不可用时,就会发生这种情况。例如,假设一个对象提供了一个同步方法,这个方法通常需要很长时间才能返回。如果一个线程频繁地调用这个方法,那么其他同样需要频繁地同步访问同一个对象的线程将经常被阻塞。
活锁
一个线程经常响应另一个线程的动作。如果其他线程的动作也是对另一个线程动作的响应,那么可能会导致livelock。与死锁一样,活动锁线程无法取得进一步的进展。然而,这些线程并没有被阻塞——它们只是忙于响应彼此而无法继续工作。这就好比两个人在走廊里互相超车:Alphonse向左移动让Gaston过去,而Gaston 向右移动让Alphonse过去。看到他们还在互相阻挡,Alphonse向右移动,而Gaston向左移动。他们还在互相阻挡,所以…
保护块
线程通常必须协调它们的动作。最常见的协调习惯用法是保护块。这样的块首先轮询一个条件,该条件必须为真,然后块才能继续。要正确地完成这项工作,需要遵循许多步骤。
例如,假设guardedJoy是一个方法,在另一个线程设置了共享变量joy之前,该方法不能继续执行。理论上,这种方法可以简单地循环直到满足条件,但这种循环是浪费时间的,因为它在等待时连续执行。
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
一个更有效的保护调用Object.wait挂起当前线程。wait的调用不会返回,直到另一个线程发出某个特殊事件可能已经发生的通知——尽管不一定是这个线程正在等待的事件:
public synchronized void guardedJoy() {
// This guard only loops once for each special event, which may not
// be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
注意:总是在循环中调用wait,以测试正在等待的条件。不要假设中断是为您等待的特定条件,或者该条件仍然为真。
和许多暂停执行的方法一样,wait也会抛出InterruptedException异常。在这个例子中,我们可以忽略这个例外——我们只关心joy的值。
为什么这个版本的guardedJoy是同步的?假设d是我们用来调用wait的对象。当线程调用d.wait时,它必须拥有d的内在锁,否则会抛出错误。在同步方法中调用wait是获取内在锁的一种简单方法。
当调用wait时,线程释放锁并暂停执行。在将来的某个时候,另一个线程将获得相同的锁并调用Object.notifyAll通知所有等待锁的线程发生了重要的事情:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
在第二个线程释放锁一段时间后,第一个线程重新获得锁,并从wait调用返回。
注意:还有第二个通知方法notify,它唤醒单个线程。因为notify不允许你指定唤醒哪个线程,所以它只在大规模并行应用程序中有用——也就是说,有大量线程的程序,所有线程都在做类似的工作。在这样的应用程序中,您不关心哪个线程被唤醒。
让我们使用保护块来创建生产者——消费者应用程序。这类应用程序在两个线程之间共享数据:创建数据的生产者和处理数据的消费者。这两个线程使用一个共享对象进行通信。协调是至关重要的:在生产者线程交付数据之前,消费者线程不能试图检索数据;如果消费者还没有检索到旧数据,生产者线程也不能试图提交新数据。
在本例中,数据是一系列文本消息,通过一个Drop类型的对象共享:
public class Drop {
// Message sent from producer
// to consumer.
private String message;
// True if consumer should wait
// for producer to send message,
// false if producer should wait for
// consumer to retrieve message.
private boolean empty = true;
public synchronized String take() {
// Wait until message is
// available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = true;
// Notify producer that
// status has changed.
notifyAll();
return message;
}
public synchronized void put(String message) {
// Wait until message has
// been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = false;
// Store message.
this.message = message;
// Notify consumer that status
// has changed.
notifyAll();
}
}
在Producer中定义的生产者线程发送一系列普通的消息。字符串“DONE”表示已发送所有消息。为了模拟真实世界应用程序不可预测的特性,生产者线程在消息之间随机暂停。
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0;
i < importantInfo.length;
i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
drop.put("DONE");
}
}
在消费者中定义的Consumer线程只是检索消息并打印出来,直到它检索到“DONE”字符串。这个线程也会随机暂停。
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take();
! message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
}
}
最后,在ProducerConsumerExample中定义的主线程,它启动生产者和消费者线程。
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
注意:Drop类是为了演示被保护的块而编写的。为了避免重复工作,在编写自己的数据共享对象之前,请检查Java Collections Framework中的现有数据结构。有关更多信息,请参阅Questions and Exercises(问题和练习)部分。
不可变对象
如果对象的状态在构造后不能改变,则该对象被认为是不可变的。对不可变对象的最大依赖被广泛认为是创建简单、可靠代码的一种良好策略。
不可变对象在并发应用程序中特别有用。由于它们不能改变状态,因此它们不能被线程冲突破坏或观察到处于不一致的状态。
程序员通常不愿意使用不可变对象,因为他们更担心创建新对象的成本,而不是在其他的时候对象被更新。创建对象的影响经常被高估,并且这些可以被一些不可变对象带来的效率所抵消。这包括减少垃圾收集带来的开销,以及消除保护可变对象免受破坏所需的代码。
下面的小节以一个实例是可变的类为例,并派生一个具有不可变实例的类。在这样做的过程中,它们给出了这种转换的一般规则,并展示了不可变对象的一些优点。
同步类示例
SynchronizedRGB类定义了表示颜色的对象。每个对象将颜色表示为三个代表主颜色值的整数和一个给出颜色名称的字符串。
public class SynchronizedRGB {
// Values must be between 0 and 255.
private int red;
private int green;
private int blue;
private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public SynchronizedRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public void set(int red,
int green,
int blue,
String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}
public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public synchronized String getName() {
return name;
}
public synchronized void invert() {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
name = "Inverse of " + name;
}
}
SynchronizedRGB必须小心使用,以避免被看到处于不一致的状态。例如,假设一个线程执行以下代码:
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
如果另一个线程调用color.set在语句1之后但在语句2之前,myColorInt的值将不匹配myColorName的值。为了避免这种结果,这两种语句必须结合在一起:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
这种不一致性只可能出现在可变对象上——对于SynchronizedRGB的不可变版本来说,这不是问题。
定义不可变对象的策略
以下规则定义了创建不可变对象的简单策略。并不是所有被记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者是草率的——他们可能有很好的理由相信他们的类的实例在构造之后永远不会改变。然而,这种策略需要复杂的分析,不适合初学者。
- 不要提供“setter”方法——修改字段或字段引用的对象的方法。
- 使所有字段为final和private。
- 不要允许子类重写方法。最简单的方法是将类声明为final。更老练的方法是将构造函数设为私有,并在工厂方法中构造实例。
- 如果实例字段包含对可变对象的引用,则不允许修改这些对象:
1)不要提供修改可变对象的方法。
2)不要共享对可变对象的引用。永远不要存储传递给构造函数的外部可变对象的引用;如果需要,创建副本,并存储对副本的引用。类似地,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。
将此策略应用于SynchronizedRGB,结果如下:
- 这个类中有两个setter方法。第一个set任意转换对象,并且在类的不可变版本中没有位置。第二个是invert,可以通过让它创建一个新对象而不是修改现有对象来调整。
- 所有字段都已经private;他们被进一步限定为final。
- 类本身被声明为final。
- 只有一个字段指向一个对象,而该对象本身是不可变的。因此,不需要防止改变“包含的”可变对象的状态。
在这些变化之后,我们有了ImmutableRGB:
final public class ImmutableRGB {
// Values must be between 0 and 255.
final private int red;
final private int green;
final private int blue;
final private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public ImmutableRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public String getName() {
return name;
}
public ImmutableRGB invert() {
return new ImmutableRGB(255 - red,
255 - green,
255 - blue,
"Inverse of " + name);
}
}
高级并发对象
到目前为止,本课的重点是低级api,这些api从一开始就是Java平台的一部分。这些api对于非常基本的任务已经足够了,但是对于更高级的任务还需要更高层次的模块。这对于充分利用当今多处理器和多核系统的大规模并发应用程序来说尤其如此。
在本节中,我们将介绍Java平台5.0版本引入的一些高级并发特性。这些特性中的大部分都在新的java.util.concurrent包中实现。Java集合框架中也有新的并发数据结构。
- 锁对象:支持简化许多并发应用程序的锁的习惯用法。
- 执行器:定义一个用于启动和管理线程的高级API。java.util.concurrent提供的Executor实现提供了适合大规模应用程序的线程池管理。
- 并发集合:使管理大型数据集合变得更容易,并且可以大大减少同步的需要。
- 原子变量:具有最小化同步并帮助避免内存一致性错误的特性。
- ThreadLocalRandom:(在JDK 7中)提供了从多个线程高效地生成伪随机数的功能。
锁对象
同步代码依赖于一种简单的可重入锁。这种锁很容易使用,但有很多限制。更复杂的的锁习惯用法由java.util.concurrent.locks包支持。我们不会详细研究这个包,而是将重点放在它最基本的接口Lock上。
Lock对象的工作方式与同步代码使用的隐式锁非常相似。与隐式锁一样,同一时间只有一个线程可以拥有一个Lock对象。Lock对象还通过它们的相关Condition对象支持wait/notify机制。
与隐式锁相比,Lock对象的最大优势是他们在试图获取锁时退出的能力。如果锁不能立即使用,或者在超时过期之前(如果指定了),tryLock方法将退出。如果另一个线程在获取锁之前发送了一个中断,那么lockinterruptible方法就会退出。
让我们使用Lock对象来解决我们在 活性 章节中看到的死锁问题。Alphonse和Gaston已经训练自己注意到朋友何时鞠躬。我们通过对Friend对象必须在两个参与者鞠躬之前获得锁之前的规定进行重新建模。下面是改进后的模型Safelock的源代码。为了展示这个用法的多功能性,我们假设Alphonse和Gaston是如此迷恋他们新发现的安全鞠躬的能力,以至于他们不停地向对方鞠躬:
public class Safelock {
static class Friend {
private final String name;
private final Lock lock = new ReentrantLock();
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public boolean impendingBow(Friend bower) {
Boolean myLock = false;
Boolean yourLock = false;
try {
myLock = lock.tryLock();
yourLock = bower.lock.tryLock();
} finally {
if (! (myLock && yourLock)) {
if (myLock) {
lock.unlock();
}
if (yourLock) {
bower.lock.unlock();
}
}
}
return myLock && yourLock;
}
public void bow(Friend bower) {
if (impendingBow(bower)) {
try {
System.out.format("%s: %s has"
+ " bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
} finally {
lock.unlock();
bower.lock.unlock();
}
} else {
System.out.format("%s: %s started"
+ " to bow to me, but saw that"
+ " I was already bowing to"
+ " him.%n",
this.name, bower.getName());
}
}
public void bowBack(Friend bower) {
System.out.format("%s: %s has" +
" bowed back to me!%n",
this.name, bower.getName());
}
}
static class BowLoop implements Runnable {
private Friend bower;
private Friend bowee;
public BowLoop(Friend bower, Friend bowee) {
this.bower = bower;
this.bowee = bowee;
}
public void run() {
Random random = new Random();
for (;;) {
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {}
bowee.bow(bower);
}
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new BowLoop(alphonse, gaston)).start();
new Thread(new BowLoop(gaston, alphonse)).start();
}
}
执行器
在前面的所有示例中,由新线程(由其Runnable对象定义)执行的任务与线程本身(由thread对象定义)之间存在密切的联系。这对于小型应用程序很有效,但是在大型应用程序中,将线程管理和创建与应用程序的其余部分分开是有意义的。封装这些函数的对象称为执行器。下面的小节详细描述执行器。
- 执行器接口:定义三种执行器对象类型。
- 线程池:是最常见的一种执行器实现。
- Fork/Join:是一个利用多处理器优势的框架(在JDK 7更新的)。
执行器接口
java.util.concurrent包定义了三个执行器接口:
- Executor:支持启动新任务的简单接口。
- ExecutorService:Executor的子接口,它增加了帮助管理生命周期的功能,包括单个任务和Executor本身。
- ScheduledExecutorService:ExecutorService的子接口,支持未来的任务 和/或 周期性执行。
通常,引用执行器对象的变量声明为这三种接口类型之一,而不是使用执行器类类型。
Executor 接口
Executor接口提供了单一的方法execute,旨在替代常见的线程创建习惯用法。如果r是一个Runnable对象,而e是一个Executor对象,您可以把(new Thread(r)).start();
替换成e.execute(r);
但是,execute的定义是比较笼统的。低级的用法是创建一个新线程并立即启动它。根据Executor实现的不同,execute可能做同样的事情,但更可能使用现有的工作线程运行r,或将r放置在队列中等待工作线程可用。(我们将在线程池一节中描述工作线程。)
java.util.concurrent中的执行器实现被设计成充分利用更高级的ExecutorService和ScheduledExecutorService接口,尽管它们也与基本的executor接口一起工作。
ExecutorService 接口
ExecutorService接口用一个类似但更通用的submit方法来补充execute。与execute一样,submit接受Runnable对象,但也接受Callable对象,该对象允许任务返回一个值。submit方法返回一个Future对象,该对象用于检索Callable返回值,并管理Callable和Runnable任务的状态。
ExecutorService还提供了提交大量Callable对象集合的方法。最后,ExecutorService提供了许多方法来管理执行器的关闭。为了支持立即关闭,任务应该正确地处理中断。
ScheduledExecutorService 接口
ScheduledExecutorService接口用schedule补充了其父ExecutorService的方法,它在指定的延迟后执行一个Runnable或Callable任务。此外,该接口定义了scheduleAtFixedRate和scheduleWithFixedDelay,它们以定义的间隔重复执行指定的任务。
线程池
java.util.concurrent中的大多数执行器实现都使用线程池,线程池由工作线程组成。这种线程与它所执行的Runnable和Callable任务分开存在,通常用于执行多个任务。
使用工作线程最小化了线程创建的开销。线程对象使用大量内存,在大规模应用程序中,分配和回收许多线程对象会产生巨大的内存管理开销。
一种常见的线程池类型是固定线程池。这种类型的池总是有指定数量的线程在运行;如果线程在仍在使用时以某种方式终止,它将自动被一个新线程替换。任务通过内部队列提交到池中,每当活动任务比线程多时,内部队列就会保存额外的任务。
固定线程池的一个重要优点是,使用它的应用程序可以优雅地降级。要理解这一点,请考虑一个web服务器应用程序,其中每个HTTP请求都由一个单独的线程处理。如果应用程序只是为每个新的HTTP请求创建一个新线程,而系统接收到的请求超过了它立即处理的能力,那么当所有这些线程的开销超过系统的能力时,应用程序将突然停止响应所有请求。由于限制了可以创建的线程的数量,应用程序将不会以HTTP请求到达时的速度来服务它们,但它将以系统所能维持的速度来服务它们。
创建使用固定线程池的执行器的一个简单方法是调用java.util.concurrent.Executors中的newFixedThreadPool工厂方法。这个类还提供了以下工厂方法:
- newCachedThreadPool方法创建了一个具有可扩展线程池的执行器。此执行程序适合于启动许多短期任务的应用程序。
- newSingleThreadExecutor方法创建一个执行器,每次执行一个任务。
- 几个工厂方法是上述执行器的ScheduledExecutorService版本。
如果上面工厂方法提供的执行器都不能满足你的需求,构造java.util.concurrent.ThreadPoolExecutor或java.util.concurrent.ScheduledThreadPoolExecutor的实例将会给你额外的选项。
Fork/Join
fork/join框架是ExecutorService接口的一个实现,它可以帮助您利用多个处理器。它是为可以递归地分解成更小块的工作而设计的。目标是使用所有可用的处理能力来增强应用程序的性能。
与任何ExecutorService实现一样,fork/join框架将任务分配给线程池中的工作线程。fork/join框架与众不同,因为它使用了work-stealing(窃取工作)的算法。工作线程用完可做的事情后,可以从其他仍然繁忙的线程窃取任务。
fork/join框架的中心是ForkJoinPool类,它是AbstractExecutorService类的扩展。ForkJoinPool实现核心work-stealing(工作窃取)算法,可以执行ForkJoinTask进程。
基本使用
使用fork/join框架的第一步是编写执行部分工作的代码。你的代码应该类似于下面的伪代码:
if (我的工作量足够小)
直接做工作
else
把我的工作分成两部分
调用这两个部分并等待结果
将这段代码包装在ForkJoinTask子类中,通常使用它的一种更专门化的类型RecursiveTask(它可以返回结果)或RecursiveAction。
在您的ForkJoinTask子类准备好之后,创建表示所有要完成的工作的对象,并将其传递给ForkJoinPool实例的invoke()方法。
模糊的清晰
为了帮助您理解fork/join框架是如何工作的,请考虑以下示例。假设您想要模糊图像。原始源图像由一个整数数组表示,其中每个整数包含单个像素的颜色值。模糊目标图像也用一个与源图像大小相同的整数数组表示。
执行模糊是通过一次对源阵列处理一个像素来完成的。每个像素与其周围的像素平均(红色、绿色和蓝色组件平均),结果放置在目标数组中。由于图像是一个大数组,这个过程可能需要很长时间。通过使用fork/join框架实现算法,您可以利用多处理器系统上的并发处理。下面是一个可能的实现:
public class ForkBlur extends RecursiveAction {
private int[] mSource;
private int mStart;
private int mLength;
private int[] mDestination;
// Processing window size; should be odd.
private int mBlurWidth = 15;
public ForkBlur(int[] src, int start, int length, int[] dst) {
mSource = src;
mStart = start;
mLength = length;
mDestination = dst;
}
protected void computeDirectly() {
int sidePixels = (mBlurWidth - 1) / 2;
for (int index = mStart; index < mStart + mLength; index++) {
// Calculate average.
float rt = 0, gt = 0, bt = 0;
for (int mi = -sidePixels; mi <= sidePixels; mi++) {
int mindex = Math.min(Math.max(mi + index, 0),
mSource.length - 1);
int pixel = mSource[mindex];
rt += (float)((pixel & 0x00ff0000) >> 16)
/ mBlurWidth;
gt += (float)((pixel & 0x0000ff00) >> 8)
/ mBlurWidth;
bt += (float)((pixel & 0x000000ff) >> 0)
/ mBlurWidth;
}
// Reassemble destination pixel.
int dpixel = (0xff000000 ) |
(((int)rt) << 16) |
(((int)gt) << 8) |
(((int)bt) << 0);
mDestination[index] = dpixel;
}
}
...
现在实现抽象compute()方法,该方法要么直接执行模糊,要么将其分割为两个较小的任务。一个简单的数组长度阈值可以帮助确定工作是执行还是分割。
protected static int sThreshold = 100000;
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
如果前面的方法是在RecursiveAction类的子类中,那么设置任务在ForkJoinPool中运行是简单的,并涉及以下步骤:
-
创建一个表示所有要完成的工作的任务。
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
-
创建将运行任务的ForkJoinPool。
ForkJoinPool pool = new ForkJoinPool();
-
运行任务。
pool.invoke(fb);
有关完整的源代码,包括一些创建目标图像文件的额外代码,请参阅ForkBlur示例。
public class ForkBlur extends RecursiveAction {
private int[] mSource;
private int mStart;
private int mLength;
private int[] mDestination;
private int mBlurWidth = 15; // Processing window size, should be odd.
public ForkBlur(int[] src, int start, int length, int[] dst) {
mSource = src;
mStart = start;
mLength = length;
mDestination = dst;
}
// Average pixels from source, write results into destination.
protected void computeDirectly() {
int sidePixels = (mBlurWidth - 1) / 2;
for (int index = mStart; index < mStart + mLength; index++) {
// Calculate average.
float rt = 0, gt = 0, bt = 0;
for (int mi = -sidePixels; mi <= sidePixels; mi++) {
int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1);
int pixel = mSource[mindex];
rt += (float) ((pixel & 0x00ff0000) >> 16) / mBlurWidth;
gt += (float) ((pixel & 0x0000ff00) >> 8) / mBlurWidth;
bt += (float) ((pixel & 0x000000ff) >> 0) / mBlurWidth;
}
// Re-assemble destination pixel.
int dpixel = (0xff000000)
| (((int) rt) << 16)
| (((int) gt) << 8)
| (((int) bt) << 0);
mDestination[index] = dpixel;
}
}
protected static int sThreshold = 10000;
@Override
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
// Plumbing follows.
public static void main(String[] args) throws Exception {
String srcName = "red-tulips.jpg";
File srcFile = new File(srcName);
BufferedImage image = ImageIO.read(srcFile);
System.out.println("Source image: " + srcName);
BufferedImage blurredImage = blur(image);
String dstName = "blurred-tulips.jpg";
File dstFile = new File(dstName);
ImageIO.write(blurredImage, "jpg", dstFile);
System.out.println("Output image: " + dstName);
}
public static BufferedImage blur(BufferedImage srcImage) {
int w = srcImage.getWidth();
int h = srcImage.getHeight();
int[] src = srcImage.getRGB(0, 0, w, h, null, 0, w);
int[] dst = new int[src.length];
System.out.println("Array size is " + src.length);
System.out.println("Threshold is " + sThreshold);
int processors = Runtime.getRuntime().availableProcessors();
System.out.println(Integer.toString(processors) + " processor"
+ (processors != 1 ? "s are " : " is ")
+ "available");
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
ForkJoinPool pool = new ForkJoinPool();
long startTime = System.currentTimeMillis();
pool.invoke(fb);
long endTime = System.currentTimeMillis();
System.out.println("Image blur took " + (endTime - startTime) +
" milliseconds.");
BufferedImage dstImage =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
dstImage.setRGB(0, 0, w, h, dst, 0, w);
return dstImage;
}
}
标准实现
除了使用fork/join框架为在多处理器系统上并发执行的任务实现自定义算法(如前一节中的ForkBlur.java示例)外,Java SE中还有一些常用的特性已经使用fork/join框架实现了。Java SE 8中引入了这样一个实现,java.util.Arrays类使用它的parallelSort()方法。这些方法类似于sort(),但通过fork/join框架利用并发性。在多处理器系统上,并行排序比顺序排序更快。然而,这些方法如何利用fork/join框架超出了Java教程的范围。有关此信息,请参阅Java API文档。
fork/join框架的另一个实现是由Java .util.streams包中的方法使用的,该包是计划在Java SE 8发行版中发布的Project Lambda的一部分。有关更多信息,请参见Lambda表达式一节。
并发集合
java.util.concurrent包包含了Java集合框架的许多附加内容。它们最容易根据提供的集合接口进行分类:
- BlockingQueue:定义一个先进先出的数据结构,当您试图添加到一个完整的队列或从一个空队列中检索时,该数据结构会阻塞或超时。
- ConcurrentMap:是java.util.Map的子接口,它定义了有用的原子操作。这些操作只有在该键存在时才会移除或替换该键值对,或者只有在该键不存在时才会添加该键值对。使这些操作原子化有助于避免同步。ConcurrentMap的标准通用实现是ConcurrentHashMap,它是HashMap的并发模拟。
- ConcurrentNavigableMap:是ConcurrentMap的一个子接口,它支持近似匹配。ConcurrentNavigableMap的标准通用实现是ConcurrentSkipListMap,它是TreeMap的并发模拟。
通过在将对象添加到集合的操作与访问或删除该对象的后续操作之间定义happens-before关系,所有这些集合都有助于避免内存一致性错误。
原子变量
java.util.concurrent.atomic包定义了支持对单个变量进行原子操作的类。所有类都有get和set方法,它们的工作方式类似于对volatile变量的读取和写入。也就是说,一个set与同一变量上的任何后续get都有happens-before关系。原子的compareAndSet方法也具有这些内存一致性特性,应用于整数原子变量的简单原子算术方法也是如此。
为了了解这个包是如何使用的,让我们回到我们最初用来演示线程干扰的Counter类:
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
使Counter安全的一种方法是使其方法同步,如SynchronizedCounter:
class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
对于这个简单的类,同步是一个可以接受的解决方案。但是对于更复杂的类,我们可能希望避免不必要的同步带来的活动影响。将int字段替换为AtomicInteger允许我们在不使用同步的情况下防止线程冲突,如AtomicCounter:
class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
并发随机数
在JDK 7中,java.util.concurrent包含一个方便的类ThreadLocalRandom,用于希望使用多线程或ForkJoinTasks中的随机数的应用程序。
对于并发访问,使用ThreadLocalRandom而不是Math.random()可以减少争用,并最终获得更好的性能。
您所需要做的就是调用ThreadLocalRandom.current(),然后调用它的一个方法来检索随机数。这是一个例子:
int r = ThreadLocalRandom.current() .nextInt(4, 77);
进一步的参考资料
- Concurrent Programming in Java: Design Principles and Pattern (2nd Edition) by Doug Lea。这是一位权威专家的综合研究成果,他也是Java平台并发框架的架构师。
- Java Concurrency in Practice by Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea.实用指南为初学者设计的实用指南。
- Effective Java Programming Language Guide (2nd Edition) by Joshua Bloch。虽然这是一本通用的编程指南,但它关于线程的章节包含了并发编程的基本“最佳实践”。
- Concurrency: State Models & Java Programs (2nd Edition), by Jeff Magee and Jeff Kramer。通过结合建模和实例介绍并发编程。
- Java Concurrent Animated:显示并发特性使用情况的动画。(链接没有了)