【翻译十四】java-并发之保护块儿

时间:2023-01-14 20:05:39

Guarded Blocks

Threads often have to coordinate their actions. The most common coordination idiom is the guarded block. Such a block begins by polling a condition that must be true before the block can proceed. There are a number of steps to follow in order to do this correctly.

Suppose, for example guardedJoy is a method that must not proceed until a shared variable joy has been set by another thread. Such a method could, in theory, simply loop until the condition is satisfied, but that loop is wasteful, since it executes continuously while waiting.

public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}

A more efficient guard invokes Object.wait to suspend the current thread. The invocation of wait does not return until another thread has issued a notification that some special event may have occurred — though not necessarily the event this thread is waiting for:

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!");
}

Note: Always invoke wait inside a loop that tests for the condition being waited for. Don't assume that the interrupt was for the particular condition you were waiting for, or that the condition is still true.

Like many methods that suspend execution, wait can throw InterruptedException. In this example, we can just ignore that exception — we only care about the value of joy.

Why is this version of guardedJoy synchronized? Suppose d is the object we're using to invoke wait. When a thread invokesd.wait, it must own the intrinsic lock for d — otherwise an error is thrown. Invoking wait inside a synchronized method is a simple way to acquire the intrinsic lock.

When wait is invoked, the thread releases the lock and suspends execution. At some future time, another thread will acquire the same lock and invoke Object.notifyAll, informing all threads waiting on that lock that something important has happened:

public synchronized notifyJoy() {
joy = true;
notifyAll();
}

Some time after the second thread has released the lock, the first thread reacquires the lock and resumes by returning from the invocation of wait.


Note: There is a second notification method, notify, which wakes up a single thread. Because notify doesn't allow you to specify the thread that is woken up, it is useful only in massively parallel applications — that is, programs with a large number of threads, all doing similar chores. In such an application, you don't care which thread gets woken up.

Let's use guarded blocks to create a Producer-Consumer application. This kind of application shares data between two threads: the producer, that creates the data, and the consumer, that does something with it. The two threads communicate using a shared object. Coordination is essential: the consumer thread must not attempt to retrieve the data before the producer thread has delivered it, and the producer thread must not attempt to deliver new data if the consumer hasn't retrieved the old data.

In this example, the data is a series of text messages, which are shared through an object of type 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();
}
}

The producer thread, defined in Producer, sends a series of familiar messages. The string "DONE" indicates that all messages have been sent. To simulate the unpredictable nature of real-world applications, the producer thread pauses for random intervals between messages.

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");
}
}

The consumer thread, defined in Consumer, simply retrieves the messages and prints them out, until it retrieves the "DONE" string. This thread also pauses for random intervals.

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) {}
}
}
}

Finally, here is the main thread, defined in ProducerConsumerExample, that launches the producer and consumer threads.

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();
}
}

Note: The Drop class was written in order to demonstrate guarded blocks. To avoid re-inventing the wheel, examine the existing data structures in the Java Collections Framework before trying to code your own data-sharing objects. For more information, refer to the Questions and Exercises section.

« Previous • Trail • Next »
 
译文:
保护块儿
  线程之间经常需要协同工作。协同最多的就是保护块儿。这样的块儿会轮询一个条件直到这个条件为真才能继续往下执行。为了保证这个执行正确这里需要一些列的步骤。
  假设,例如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 方法挂起当前线程。这个线程不会返回直到其他的线程执行了一些特殊的操作并唤醒它,虽然这个线程并不一定在等待。

 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();
}

许多时候当第二个线程释放了这个锁,第一个线程会重新获得锁等待返回调用。



注意:这里有第二个唤起注意的方法,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”表示所有的消息都已经发送。为了模拟现实世界的生产者-消费者现象,生产者线程会在发送下一个消息的时候停留随机的时间。

 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线程中定义,它载入了Producer和Consumer类。

 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 节。


养养眼^-^
【翻译十四】java-并发之保护块儿