未验证 提交 a6280635 编写于 作者: 飞龙 提交者: GitHub

Merge pull request #13 from Ruffianjiang/master

第十章 翻译结束
# 第10章 并发和同步
> 译者:[Ruffianjiang](https://github.com/Ruffianjiang)
​ 到目前为止,我们所做的每件事都隐含着一个假设,即一个程序正在修改我们的数据结构。在Java中,由于线程的存在,可能会产生多个程序使对象的效果
​ 尽管用于描述线程的语言表明,线程的目的是允许同时发生几件事情,但这在一定程度上是一种误导。即使是最小的Java应用程序运行在 Sun 的 JDK 的平台上,例如,有五个线程,这是只有在应用程序没有创建任何本身,即使程序运行的机器由一个单处理器(一次只能执行一个指令)。这四个额外的“系统线程”执行许多任务(例如“finalizing”对象是程序不再可访问),这些任务在逻辑上独立于程序的其余部分。相对于程序的其余部分,它们的操作可以在任何时候有效地发生。换句话说,Sun的Java运行时系统使用线程作为其系统的组织工具。
​ 使用图形用户界面(GUIs)的Java程序中有大量线程。一个线程绘制或重绘屏幕。另一个响应事件,如点击鼠标按钮在屏幕上的某个点。这些都是相关的,但在很大程度上是独立的活动:例如,当一个窗口变得不可见并显示它们时,必须重新绘制对象,这与程序正在执行的任何计算无关。线程违反了我们的隐含假设,即一个程序对我们的数据进行操作,因此,即使是一个完全实现的数据结构,其所有实例变量都是私有的,也可能以相当奇怪的方式受到破坏。在相同数据对象上运行的多个线程的存在也提出了一个普遍的问题,即这些线程如何以有序的方式彼此通信。
## 10.1 同步的数据结构
参考4.1节 `ArrayList` 的实现。在方法 `ensureCapacity` 中,我们发现
```java
public void ensureCapacity(int N) {
if (N <= data.length)
return;
Object[] newData = new Object[N];
System.arraycopy (data, 0,newData, 0, count);
data = newData;
}
```
```java
public Object set(int k, Object x) {
check (k, count);
Object old = data[k];
data[k] = x;
return old;
}
```
​ 假设一个程序执行 `ensureCapacity` ,而另一个程序正在相同的 `ArrayList` 对象上执行改动。我们可以看到他们的动作交叉在了一起,如下所示:
```java
/* Program 1 executes: */ newData = new Object[N];
/* Program 1 executes: */ System.arraycopy (data, 0,newData, 0, count);
/* Program 2 executes: */ data[k] = x;
/* Program 1 executes: */ data = newData;
```
​ 因此,我们丢失了 `Program 2` 设置的值,因为它在 `data` 的内容复制到新的扩展数组之后,将这个值放入 `data` 的旧值中。为了解决 `ArrayList` 呈现出的简单问题,线程可以以互斥的方式安排访问任何特定的 `ArrayList` —— 也就是说,每次只有一个线程操作对象。Java的 `synchronized` 语句提供互斥,允许我们生成 `synchronized` (或线程安全)数据结构。下面是一个例子的一部分,展示了`synchronized`方法修饰符的使用和同步语句的等效使用:
```java
public class SyncArrayList<T> extends ArrayList<T> {
...
public void ensureCapacity(int n) {
synchronized (this) {
super.ensureCapacity (n);
}
}
public synchronized T set(int k, T x) {
return super.set (k, x);
}
...
}
```
​ 为所有方法提供这样的包装函数清单的过程非常繁琐,以至于标准Java库类的 `java.util.Collections` 提供了以下方法:
```java
/** A synchronized (thread-safe) view of the list L, in which only
* one thread at a time executes any method. To be effective,
* (a) there should be no subsequent direct use of L,
* and (b) the returned List must be synchronized upon
* during any iteration, as in
*
* List aList = Collections.synchronizedList(new ArrayList());
* ...
* synchronized(aList) {
* for (Iterator i = aList.iterator(); i.hasNext(); )
* foo(i.next());
* }
*/
public static List<T> synchronizedList (List L<T>) { ... }
```
​ 不幸的是,每个操作的同步都有时间开销,这就是为什么Java库设计人员决定不同步 `Collection` 及其大多数子类型。另一方面,`StringBuffers``Vectors` 是同步的,不能被同时使用而损坏。
## 10.2 监控与有序通信
`synchronizedList` 方法返回的对象是最简单的监视器的例子。这个术语指的是控制(“监视器”)对某些数据结构的并发访问以使其正确工作的对象(或对象类型)。监视器的一个功能是在需要时提供对数据结构操作的互斥访问。另一种方法是安排线程之间的同步——这样一个线程就可以等待对象“准备好”为它提供一些服务。
​ 监视器可以用一个典型的例子来说明:共享缓冲区或邮箱。其公共规范的一个简单版本如下:
```java
/** A container for a single message (an arbitrary Object). At any
* time, a SmallMailbox is either empty (containing no message) or
* full (containing one message). */
public class SmallMailbox {
/** When THIS is empty, set its current message to MESSAGE, making
* it full. */
public synchronized void deposit (Object message)
throws InterruptedException { ... }
/** When THIS is full, empty it and return its current message. */
public synchronized Object receive ()
throws InterruptedException { ... }
}
```
​ 自规范表明这两种方法都可能需要等待新的消息沉积或接收旧,我们尽可能地将两者都指定为抛出 `InterruptedException` ,这是标准Java方式表明,我们在等待时,其他线程打断我们。
​ “SmallMailbox” 规范说明了典型监视器的特性:
- 没有任何可修改的状态变量(即,字段)是公开的。
- 从单独的线程访问,使任何对可修改状态的引用相互排除;每次只有一个线程持有 `SmallMailbox` 对象上的锁。
- 线程可以暂时放弃锁,等待某些更改的通知。但是锁的所有权的变化只发生在程序中定义良好的点上。
内部表示很简单:
```java
private Object message;
private boolean amFull;
```
​ 这些实现使用了基本的Java特性来“等待直到被通知”:
```java
public synchronized void deposit (Object message)
throws InterruptedException{
while (amFull)
wait (); // Same as this.wait ();
this.message = message;
this.amFull = true;
notifyAll (); // Same as this.notifyAll ()
}
public synchronized Object receive ()
throws InterruptedException{
while (! amFull)
wait ();
amFull = false;
notifyAll ();
return message;
}
```
`SmallMailbox` 的方法只允许其他线程进入精心控制的点:等待的调用。例如,`deposit` 中的循环表示“如果仍然有旧的未接收邮件,请等到其他线程接收到它并再次唤醒我(使用 `notifyAll` ),然后我再次锁定了这个邮箱”。从执行 `deposit``receive` 的线程的角度来看,每次调用 `wait` 都会对 `this` 的实例变量造成一些变化——可能会受到其他调用 `deposit``receive` 的影响。
​ 只要程序的线程小心翼翼地以这种方式保护它们在监视器中的所有数据,它们就会避免 10.1节开头描述的那种奇怪的交互。当然,天下没有免费的午餐;使用锁会导致死锁的情况,即两个或多个线程无限期地等待对方,如下面的人为例子所示:
```java
class Communicate {
static SimpleMailbox
box1 = new SimpleMailbox (),
box2 = new SimpleMailbox ();
}
// Thread #1: | // Thread #2:
m1 = Communicate.box1.receive ();| m2 = Communicate.box2.receive();
Communicate.box2.deposit (msg1); | Communicate.box1.deposit (msg2);
```
​ 由于两个线程在试图从其消息框中接收消息之前都不发送任何消息,所以两个线程都彼此等待(可以通过让其中一个线程反转接收和存储消息的顺序来解决这个问题)。
## 10.3 消息传递
​ 监视器(Monitors)为多个线程提供了一种有序的方法来访问数据,而不会互相阻碍。隐藏在监视器概念背后的是一个简单的想法:
> 考虑到同时执行多个程序是很困难的,所以不要这样做!相反,编写一组单线程程序,让它们彼此交换数据
​ 对于一般监视器,“交换数据”意味着设置每个监视器都可以看到的变量。如果我们更进一步,我们可以将“交换数据”定义为“读取输入和写入输出”。“我们有一个并发的编程规则,叫做消息传递。
​ 在消息传递的世界中,线程是独立的顺序程序,而不是互相发送消息。他们读写消息使用是在 Java 的 `reader` 上读或在 Java 的 `PrintStreams` 上打印一致的方法。因此,只有当一个线程费力的“读取其消息”时,才会受到另一个线程的影响。
​ 我们可以通过编写线程来实现消息传递的效果,通过邮箱来实现所有的交互。也就是说,线程共享一组邮箱,但是不共享其他可修改的对象或变量(不可修改的对象,比如`Strings`,可以共享)
## 练习
**10.1** 给10.1节的 `Collections.synchronizedList` 提供一个可能的静态方法的实现。
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册