原创翻译整理标注,转载请注明出处
并发
Java平台的设计初衷就是为了支持并发编程,在Java编程语言和Java类库中提供基本的并发支持。从5.0版开始,Java平台还包含高级并发API。
进程和线程
在并发编程中,有两个基本的执行单元:进程和线程。在Java编程语言中,并发编程主要涉及线程。
计算机系统通常具有许多活动进程和线程。即使在只有一个执行核心的系统中也是如此,因此在任何给定时刻只有一个线程实际执行。通过称为时间切片的OS功能,在进程和线程之间共享单个核心的处理时间。
对于具有多个处理器或具有多个执行核心的处理器的计算机系统来说,处理更加自然。这极大地增强了系统并发执行进程和线程的能力 - 但即使在没有多个处理器或执行核心的简单系统上也可以实现并发。
进程
进程具有自包含的执行环境。进程通常具有完整的私有基本运行时资源集; 特别是,每个进程都有自己的内存空间。
进程通常被视为程序或应用程序的同义词。但是,用户看到的单个应用程序实际上可能是一组协作进程。为了促进进程之间的通信,大多数操作系统都支持进程间通信(IPC)资源,例如管道和套接字。IPC不仅用于同一系统上的进程之间的通信,而且还用于不同系统上的进程。
Java虚拟机的大多数实现都作为单个进程运行。Java应用程序可以使用ProcessBuilder对象创建其他进程
线程
线程有时被称为轻量级进程。进程和线程都提供执行环境,但创建新线程所需的资源更少。
线程存在于进程中 - 每个进程至少有一个进程。线程共享进程的资源,包括内存和打开文件。这使得有效但可能有问题的通信成为可能。
多线程执行是Java平台的基本特性。每个应用程序至少有一个线程 - 或几个,如果你计算“系统”线程,它们执行内存管理和信号处理等操作。但是从应用程序员的角度来看,++你只从一个线程开始,称为主线程++。该线程具有创建其他线程的能力。
线程对象
每个线程都与该类的实例相关联 Thread。使用Thread对象创建并发应用程序有两种基本策略。
- 要直接控制线程创建和管理,只需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类定义了大量的线程管理的方法。这些static方法包括提供有关调用方法的线程的信息或影响其状态的方法。从管理线程和Thread对象所涉及的其他线程调用其他方法。我们将在以下部分中研究其中一些方法。
暂停执行与睡眠
Thread.sleep可以使当前线程暂停执行。这是使处理器时间可用于应用程序的其他线程或其他应用程序的有效方法。
提供了两个重载版本sleep:一个指定睡眠时间为毫秒,另一个为纳秒。但是,这些睡眠时间并++不保证精确++,因为它们受到底层操作系统的限制。此外,睡眠周期可以通过中断终止,我们将在后面看到。在任何情况下,您都不能假设调用sleep将在指定的时间段内暂停线程。
该 SleepMessages示例用于sleep以四秒为间隔打印消息:
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声明它throws InterruptedException。当另一个线程在sleep活动时中断当前线程时,抛出此异常。由于此程序尚未定义另一个引起中断的线程,因此无需捕获InterruptedException。
中断
一个中断是一个指示线程它应该终止它在做的和将要做的。由程序员决定线程如何响应,但一般是终止线程。
通过调用interrupt可以进行中断。为使中断机制正常工作,++被中断的线程必须支持自己的中断++。
支持中断
线程如何支持自己的中断?如果线程经常调用抛出InterruptedException的方法,它只是++简单返回++。例如,假设示例中SleepMessages的run方法中Runnable。然后可以按如下方式修改以支持中断:
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,如果收到中断则返回++。例如:
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,中断状态被清除。非静态isInterrupted方法(一个线程用于查询另一个线程的中断状态)不会更改中断状态标志。
按照惯例,任何++通过抛出InterruptedException都会清除中断状态++。但是,通过另一个线程调用interrupt,立即再次设置中断状态。
Joins
该join方法允许一个线程等待另一个线程的完成。如果t是Thread其线程当前正在执行的对象,
t.join();
导致当前线程暂停执行,直到t线程终止。重载join允许程序员指定等待期。但是,如同sleep,join取决于操作系统的计时,所以你不应该假设join只要你指定就等待。
就像sleep,join通过退出来响应中断InterruptedException。
SimpleThreads示例
以下示例汇总了本节的一些概念。 SimpleThreads由两个线程组成。第一个是每个Java应用程序都有的主线程。主线程从Runnable对象创建一个新线程MessageLoop,并等待它完成。如果MessageLoop线程需要很长时间才能完成,主线程会中断它。
该MessageLoop线程打印出一系列消息。如果在打印完所有消息之前中断,则MessageLoop线程会打印一条消息并退出。
//@see com.example.zjz.javaconcurrence.demo.SimpleThreads
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运行时更慢地执行一个或多个线程,甚至暂停执行时,会发生线程竞争。 饥饿和活锁是线程竞争的形式。
本节包括以下主题:
- 线程干扰 描述了当多个线程访问共享数据时如何引入错误。
- 内存一致性错误 描述了由共享内存的不一致视图导致的错误。
- 同步方法 描述了一种简单的习惯用法,可以有效地防止线程干扰和内存一致性错误。
- 隐式锁定和同步 描述了更通用的同步习惯用法,并描述了同步基于隐式锁定的方式。
- Atomic Access 讨论了其他线程无法干扰的操作的一般概念。
线程干扰(Thread Interference)
考虑一个叫做的简单类 Counter
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter的设计是为了每次调用increment都会c加1 ,每次调用decrement都会从中减去1 。但是,如果Counter从多个线程引用对象,则线程之间的干扰可能会阻止这种情况按预期发生。
当两种操作,在不同的线程运行,但作用于同一数据的干扰情况。这意味着这两个操作由多个步骤组成,并且步骤序列重叠。
Counter对于交错实例的操作似乎不太可能,因为两个操作c都是单个简单的语句。但是,即使是简单的语句也可以由虚拟机转换为多个步骤。我们不会检查虚拟机采取的具体步骤 - 只需知道单个表达式c++可以分解为三个步骤:
- 检索当前值c。
- 将检索的值增加1。
- 将增加的值存储回来c。
表达式c–可以以相同的方式分解,除了第二步减少而不是增量。
假设线程A increment在线程B调用的同时调用decrement。如果初始值为cis 0,则它们的交错操作可能遵循以下顺序:
- 线程A:检索c。
- 线程B:检索c。
- 线程A:增加检索值; 结果是1。
- 线程B:减少检索值; 结果是-1。
- 线程A:将结果存储在c中; c现在是1。
- 线程B:将结果存储在c中; c现在是-1。
线程A的结果丢失,被线程B覆盖。这种特殊的交错只是一种可能性。在不同的情况下,可能是线程B的结果丢失,或者根本没有错误。因为它们是不可预测的,所以难以检测和修复线程干扰错误。
内存一致性错误(Memory Consistency Errors)
当不同的线程具有应该是相同数据的不一致视图时,会发生内存一致性错误。内存一致性错误的原因很复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需要的只是避免它们的策略。
避免内存一致性错误的关键是理解先发生关系(happens-before relationship)。这种关系只是保证一个特定语句的内存写入对另一个特定语句可见。要查看此内容,请考虑以下示例。假设int定义并初始化了一个简单字段:
int counter = 0;
该counter字段在两个线程A和B之间共享。假设线程A递增counter:
counter++;
然后,不久之后,线程B打印出来counter:
System.out.println(counter);
如果两个语句已在同一个线程中执行,则可以安全地假设打印出的值为“1”。但是如果这两个语句是在不同的线程中执行的,那么打印出的值可能是“0”,因为不能保证线程A的更改counter对于线程B是可见的 - 除非程序员在这些之间建立了先发生关系。两个陈述。
有几种行为可以创造先发生过的关系。其中之一是同步,我们将在以下部分中看到。
我们已经看到了两种创造前发生关系的行为。
- 当一个语句调用时Thread.start,与该语句有一个happens-before关系的每个语句也与新线程执行的每个语句都有一个happens-before关系。新线程可以看到导致创建新线程的代码的影响。
- 当一个线程终止并导致Thread.join另一个线程返回时,终止线程执行的所有语句与成功连接后的所有语句都有一个happens-before关系。现在,执行连接的线程可以看到线程中代码的效果。
有关创建先发生关系的操作列表,请参阅java.util.concurrent。
同步方法
Java编程语言提供了两种基本的同步习惯用法:synchronized方法和synchronized语句。下两节将介绍两个同步语句中较为复杂的语句。本节介绍同步方法。
要使方法同步,只需将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,则使这些方法同步有两个影响:
- 首先,对同一对象的两个同步方法的调用不可能进行交错。当一个线程正在为对象执行同步方法时,所有其他线程调用同一对象的同步方法(暂停执行)直到第一个线程完成对象。
- 其次,当同步方法退出时,它会自动与同一对象的同步方法的任何后续调用建立先发生关系。这可以保证对所有线程都可以看到对象状态的更改。
请注意,构造函数无法同步 - 使用synchronized带有构造函数的关键字是语法错误。同步构造函数没有意义,因为只有创建对象的线程在构造时才能访问它。
警告: 构造将在线程之间共享的对象时,要非常小心,对对象的引用不会过早“泄漏”。例如,假设您要维护一个包含每个类实例的List调用instances。您可能想要将以下行添加到构造函数中:
instances.add(this);
但是其他线程可以instances在构造对象完成之前用来访问对象。
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过synchronized方法完成的。(一个重要的例外:final在构造对象之后无法修改的字段,一旦构造了对象,就可以通过非同步方法安全地读取)这种策略是有效的,但是可能会带来++性能++问题。
内部锁定和同步
同步是围绕称为内部锁或监视器锁的内部实体构建的。(API规范通常将此实体简称为“监视器(monitor)”。)内部锁在同步的两个方面都发挥作用:强制对对象状态进行独占访问,并建立对可见性至关重要的先发生关系(happens-before relationships)。
每个对象都有一个与之关联的内部锁。按照惯例,需要对对象字段进行独占和一致访问的线程必须在访问对象之前获取对象的内部锁,然后在完成它们时释放内部锁。据说一个线程在获得锁定和释放锁定之间拥有内在锁定。只要一个线程拥有一个内部锁,没有其他线程可以获得相同的锁。另一个线程在尝试获取锁时将阻塞。
当线程释放内部锁时,在该操作与同一锁的任何后续获取之间建立先发生关系。
锁定同步方法
当线程调用synchronized方法时,它会自动获取该方法对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会发生锁定释放。
您可能想知道在调用静态同步方法时会发生什么,因为静态方法与类关联,而不是与对象关联。在这种情况下,线程获取Class与类关联的对象的内部锁。因此,对类的静态字段的访问由与该类的任何实例的锁不同的锁控制。
同步语句
创建同步代码的另一种方法是使用synchronized语句。与synchronized方法不同,synchronized语句必须指定提供内部锁的对象:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在该示例中,addName方法需要同步变化lastName和nameCount,还需要避免的其他对象的方法同步调用。(从同步代码调用其他对象的方法可能会产生在Liveness一节中描述的问题。)如果没有synchronized语句,则必须有一个单独的,不同步的方法,仅用于调用nameList.add。
同步语句对于通过细粒度同步 提高并发性也很有用。例如,假设class 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++;
}
}
}
++谨慎使用++。您必须绝对确保对受影响字段的访问进行交错是否安全。
可重入同步(Reentrant Synchronization)
回想一下,线程无法获取另一个线程拥有的锁。但是,一个线程可以获取它已经拥有的锁。允许线程多次获取相同的锁可启用重入同步。这描述了一种情况,其中同步代码直接或间接地调用也包含同步代码的方法,并且两组代码使用相同的锁。++在没有可重入同步的情况下,同步代码必须采取许多额外的预防措施,以避免线程导致自身阻塞。++
原子访问
在编程中,原子动作是一次有效发生的动作。原子动作不能在中间停止:它要么完全发生,要么根本不发生。在动作完成之前,原子动作的副作用是不可见的。
我们已经看到增量表达式,例如c++,没有描述原子动作。即使非常简单的表达式也可以定义可以分解为其他操作的复杂操作。但是,您可以确定下列原子操作:
- 读取和写入对于引用变量和大多数原始变量(除了long和double之外的所有类型)都是原子的。
- 读取和写入对于声明的所有变量volatile(包括 long和double变量)都是原子的。
原子动作不能交错,因此可以使用它们而不必担心线程干扰。但是,这并不能消除所有同步原子操作的需要,因为仍然可能存在内存一致性错误。使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会建立与之后读取同一变量的先发生关系。这意味着对volatile变量的更改始终对其他线程可见。更重要的是,它还意味着当一个线程读取一个volatile变量时,它不仅会看到最新的变化volatile,而且还会看到导致变化的代码的副作用。
使用简单的原子变量访问比通过同步代码访问这些变量更有效,但程序员需要更加小心以避免内存一致性错误。额外的努力是否值得取决于应用程序的大小和复杂性。
包中的某些类 java.util.concurrent提供了不依赖于同步的原子方法。
活跃度(Liveness)
并发应用程序及时执行的能力被称为其活跃性。本节描述了最常见的活动问题,即死锁,并继续简要描述其他两个活动问题,饥饿和活锁。
死锁
死锁描述了两个或多个线程永远被阻塞,等待彼此的情况。这是一个例子。
阿方斯和加斯顿是朋友,他们都很有礼貌。严格的礼貌规则是,当你向朋友鞠躬时,你必须保持鞠躬,直到你的朋友有机会还礼。不幸的是,这条规则没有考虑到两个朋友可能同时互相鞠躬的可能性。这个示例应用程序 Deadlock模拟了这种可能性:
//@see com.example.zjz.javaconcurrence.demo.Deadlock
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();
}
}
当Deadlock运行时,这是非常有可能的是,当他们尝试调用两个线程将被阻塞bowBack。这两个块都不会结束,因为每个线程都在等待另一个线程退出bow。
饥饿和活锁
饥饿和活锁没有死锁常见,但仍然是每个并发软件设计者可能遇到的问题。
饥饿
Starvation描述了一种情况,即线程无法定期访问共享资源,并且无法运行。当“贪婪”线程使共享资源长时间不可用时会发生这种情况。例如,假设一个对象提供了一个通常需要很长时间才能返回的同步方法。如果一个线程经常调用此方法,则通常还需要阻止对同一对象进行频繁同步访问的其他线程。
活锁
线程通常用于响应另一个线程的操作。如果另一个线程的操作也是对另一个线程的操作的响应,则可能导致活锁。与死锁一样,活锁线程无法取得进一步进展。但是,线程没有被阻塞- 它们只是太忙于相互响应以恢复工作。这相当于两个试图在走廊里互相通过的人:Alphonse向左移动让Gaston通过,而Gaston向右移动让Alphonse通过。看到他们仍在相互阻挡,Alphone向右移动,而Gaston向左移动。他们还在互相阻挡,所以…
守卫块(Guarded Blocks)
线程通常必须协调他们的行为。最常见的协调习语是守卫块。这样的块开始于在块可以继续之前轮询必须为真的条件。要正确执行此操作,需要执行许多步骤。
例如,假设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在synchronized方法中调用是获取内部锁的简单方法。
当wait被调用时,线程释放锁,并暂停执行。在将来的某个时间,另一个线程将获取相同的锁并调用 Object.notifyAll,通知等待该锁的所有线程发生了重要的事情:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
在第二个线程释放锁之后的一段时间,第一个线程重新获取锁并通过从调用返回来恢复wait。
注意: 有第二种通知方法notify,可以唤醒单个线程。++因为notify不允许您指定被唤醒的线程++,所以它仅在大规模并行应用程序中有用 - 即具有大量线程的程序,所有程序都执行类似的工作。在这样的应用程序中,您不关心哪个线程被唤醒。
让我们使用受保护的块来创建Producer-Consumer应用程序。这种应用程序在两个线程之间共享数据:生成器,创建数据,以及使用它的消费者。两个线程使用共享对象进行通信。协调是必不可少的:消费者线程不得在生产者线程交付之前尝试检索数据,并且如果消费者未检索旧数据,则生产者线程不得尝试传递新数据。
在此示例中,数据是一系列文本消息,通过以下类型的对象共享 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”表示已发送所有消息。为了模拟真实世界应用程序的不可预测性,生产者线程暂停消息之间的随机间隔。
import java.util.Random;
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”字符串。该线程也会暂停随机间隔。
import java.util.Random;
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中的现有数据结构 。
不可变对象
如果一个对象的状态在构造后不能改变,则该对象被认为是不可变的。最大程度上依赖不可变对象被广泛接受为创建简单,可靠代码的合理策略。
不可变对象在并发应用程序中特别有用。由于它们不能改变状态,因此它们不会被线程干扰破坏或在不一致状态下被观察到。
程序员通常不愿意使用不可变对象,因为他们担心创建新对象的成本而不是更新对象。++对象创建的影响经常被高估,并且可以通过与不可变对象相关联的一些效率来抵消。这些包括由于垃圾收集而减少的开销,以及消除保护可变对象免于损坏所需的代码。++
以下小节采用其实例可变的类,并从中派生出具有不可变实例的类。通过这样做,它们为这种转换提供了一般规则,并展示了不可变对象的一些优点。
同步类示例
该类 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++。更复杂的方法是private在工厂方法中构造构造函数和构造实例。
- 如果实例字段包含对可变对象的引用,则不允许更改这些对象:
- 不要提供修改可变对象的方法。
- 不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用; 如有必要,创建副本并存储对副本的引用。同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。
将此策略应用于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);
}
}
高级并发对象
到目前为止,本课程重点关注从一开始就是Java平台一部分的低级API。这些API适用于非常基本的任务,但更高级的任务需要更高级别的构建块。对于充分利用当今多处理器和多核系统的大规模并发应用程序尤其如此。
在本节中,我们将介绍Java平台5.0版中引入的一些高级并发功能。大多数这些功能都在新java.util.concurrent包中实现。Java Collections Framework中还有新的并发数据结构。
- 锁对象支持锁定习惯用法,简化了许多并发应用程序。
- 执行程序定义用于启动和管理线程的高级API。提供的执行程序实现提供java.util.concurrent适用于大规模应用程序的线程池管理。
- 并发集合使管理大量数据更容易,并且可以大大减少同步需求。
- 原子变量具有最小化同步的功能,有助于避免内存一致性错误。
- ThreadLocalRandom (在JDK 7中)提供了从多个线程有效生成伪随机数。
锁对象(Lock Objects)
同步代码依赖于一种简单的可重入锁定。这种锁易于使用,但有许多限制。java.util.concurrent.locks包装支持更复杂的锁定习语 。我们不会详细介绍这个包,而是将重点放在它最基本的接口上 Lock。
Lock对象的工作方式与同步代码使用的隐式锁非常相似。与隐式锁一样,一次只有一个线程可以拥有一个Lock对象。Lock对象还通过其关联Condition对象支持wait/notify机制。
++Lock对象相对于隐式锁定的最大优点是它们能够退出获取锁定的尝试++。tryLock如果锁定立即不可用或超时到期之前(如果指定),则该方法退出。lockInterruptibly如果另一个线程在获取锁之前发送中断,则该方法退出。
让我们使用Lock对象来解决我们在Liveness中看到的死锁问题。当朋友即将鞠躬时,阿方斯和加斯顿已经训练自己注意到了。我们通过要求我们的Friend对象必须在继续执行弓之前获取两个参与者的锁来模拟这种改进。以下是改进模型的源代码 Safelock。为了证明这个习语的多样性,我们假设阿尔方斯和加斯顿如此迷恋他们新发现的安全鞠躬能力,他们不能停止相互鞠躬:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
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();
}
}
执行者(Executors)
在前面的所有示例中,由Runnable对象定义的新线程和由对象定义的线程本身完成的任务之间存在紧密的联系Thread。这适用于小型应用程序,但在大型应用程序中,将线程管理和创建与应用程序的其余部分分开是有意义的。封装这些函数的对象称为执行程序(Executors)。以下小节详细描述了执行程序。
- Executor Interfaces定义三个执行器对象类型。
- 线程池是最常见的执行器实现类型。
- Fork / Join是一个利用多个处理器的框架(JDK 7中的新增功能)。
执行器接口
该java.util.concurrent包定义了三个执行器接口:
- Executor,一个支持启动新任务的简单接口。
- ExecutorService,Executor的子接口,增加了有助于管理生命周期的功能,包括单个任务和执行程序本身。
- ScheduledExecutorService,ExecutorService的子接口,支持未来和/或定期执行任务。
通常,引用executor对象的变量被声明为这三种接口类型之一,而不是执行器类类型。
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接口补充了execute相似的,但更通用的submit方法。就像execute,submit接受Runnable对象,但也接受 Callable允许任务返回值的对象。该submit方法返回一个 Future对象,该对象用于检索Callable返回值并管理两者Callable和Runnable任务的状态。
ExecutorService还提供了提交大量Callable对象的方法。最后,ExecutorService提供了许多用于管理执行程序关闭的方法。为了支持立即关闭,任务应该正确处理中断。
ScheduledExecutorService
该 ScheduledExecutorService接口补充其父接口ExecutorService schedule方法 ,其执行Runnable或Callable在指定延迟后的任务。此外,接口定义scheduleAtFixedRate并scheduleWithFixedDelay以规定的间隔重复执行指定的任务。
线程池
大多数执行程序实现都在java.util.concurrent使用线程池,它由工作线程组成。这种线程Runnable与Callable它执行的任务分开存在,通常用于执行多个任务。
使用工作线程可以最大限度地减少由于创建线程而产生 线程对象使用的大量内存,而在大型应用程序中,分配和释放许多线程对象会产生大量的内存管理开销。
一种常见类型的线程池是固定线程池。这种类型的池总是运行指定数量的线程; 如果某个线程在仍在使用时以某种方式终止,它将自动替换为新线程。任务通过内部队列提交到池中,只要有多个活动任务而不是线程,该队列就会保存额外的任务。
固定线程池的一个重要优点是使用它的应用程序可以优雅地降级。要理解这一点,请考虑一个Web服务器应用程序,其中每个HTTP请求都由一个单独的线程处理。如果应用程序只为每个新的HTTP请求创建一个新线程,并且系统接收的请求数超过它可以立即处理的数量,那么当所有这些线程的开销超过系统容量时,应用程序将突然停止响应所有请求。由于可以创建的线程数量有限制,应用程序不会像它们进入时那样快速地为HTTP请求提供服务,但它将在系统可以维持的时间内尽快为它们提供服务。
创建使用固定线程池的执行程序的一种简单方法是调用 newFixedThreadPool工厂方法。 java.util.concurrent.Executors此类还提供以下工厂方法:
- 该 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实现核心工作窃取算法并可以执行 ForkJoinTask进程。
基本用法
使用fork / join框架的第一步是编写执行一部分工作的代码。您的代码应类似于以下伪代码:
如果(我的工作部分足够小)
直接做这项工作
否则
把我的工作分成两部分
调用这两个部分并等待结果
将此代码包装在ForkJoinTask子类中,通常使用其中一种更专业的类型 RecursiveTask(可以返回结果)或 RecursiveAction。
在您的ForkJoinTask子类是准备好了,创建一个表示要完成所有的工作对象,把它传递给一个ForkJoinPool实例的invoke()方法。
模糊为Clarity
为了帮助您了解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类中,那么将任务设置为在a中运行ForkJoinPool很简单,并涉及以下步骤:
- 创建一个代表要完成的所有工作的任务。
//源图像像素在src中
//目标图像像素在dst中
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
- 创建ForkJoinPool将运行任务的那个。
ForkJoinPool pool = new ForkJoinPool();
- 运行任务。
pool.invoke(fb);
有关完整源代码,包括一些创建目标映像文件的额外代码,请参阅 ForkBlur示例。
标准实现
除了使用fork / join框架来实现在多处理器系统上同时执行的任务的自定义算法(例如ForkBlur.java上一节中的示例)之外,Java SE中有一些通常有用的功能,它们已经使用fork / join实现了框架。在Java SE 8中引入的一种这样的实现被 java.util.Arrays类用于其parallelSort()方法。这些方法类似于sort(),但通过fork / join框架利用并发性。在多处理器系统上运行时,大型阵列的并行排序比顺序排序更快。但是,这些方法如何利用fork / join框架超出了Java Tutorials的范围。有关此信息,请参阅Java API文档。
fork / join框架的另一个实现由java.util.streams包中的方法使用,该方法是为Java SE 8发布而安排的Project Lambda的一部分。
并发集合
该java.util.concurrent软件包包括Java Collections Framework的许多新增功能。这些最容易通过提供的集合接口进行分类:
- BlockingQueue 定义先进先出数据结构,当您尝试添加到完整队列或从空队列中检索时,该数据结构会阻塞或超时。
- ConcurrentMap是java.util.Map定义有用原子操作的子接口 。仅当密钥存在时,这些操作才会删除或替换键值对,或仅在密钥不存在时才添加键值对。使这些操作原子化有助于避免同步。标准的通用实现ConcurrentMap是 ConcurrentHashMap,它是一个并发的模拟 HashMap。
- ConcurrentNavigableMap是ConcurrentMap支持近似匹配的子接口。标准的通用实现ConcurrentNavigableMap是 ConcurrentSkipListMap,它是一个并发的模拟 ++TreeMap++。
所有这些集合通过定义将对象添加到集合的操作与访问或删除该对象的后续操作之间的先发生关系来帮助避免内存一致性错误。
原子变量
该 java.util.concurrent.atomic包定义了支持单个变量的原子操作的类。所有类都有get和set类似读取和写入volatile变量的方法。也就是说,a set与get同一变量上的任何后续关系具有先发生关系。原子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;
}
}
对于这个简单的类,同步是可接受的解决方案。但是对于更复杂的类,我们可能希望避免不必要的同步对活动的影响。使用a替换int字段AtomicInteger允许我们在不诉诸同步的情况下防止线程干扰,如 AtomicCounter:
import java.util.concurrent.atomic.AtomicInteger;
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);
进一步阅读
- Java Concurrent Animated:显示并发功能用法的动画。