1. 死锁
死锁描述了这样一种情景,两个或多个线程永久阻塞,互相等待对方释放资源。下面是一个例子。
Alphone和Gaston是朋友,都很讲究礼节。礼节有一个严格的规矩,当你向一个朋友鞠躬时,你必须保持鞠躬的姿势,直到你的朋友有机会回鞠给你。不幸的是,这个规矩没有算上两个朋友相互同时鞠躬的可能。
下面的应用例子,DeadLock,模拟了这个可能性。
当DeadLock运行后,两个线程极有可能阻塞,当它们尝试调用bowBack方法时。没有哪个阻塞会结束,因为每个线程都在等待另一个线程退出bow方法。
2. 饥饿和活锁
饥饿和活锁并不如死锁一般普遍,但它仍然是每个并发程序设计者可能会遇到的问题。
饥饿
饥饿是指当一个线程不能正常的访问共享资源并且不能正常执行的情况。这通常在共享资源被其他“贪心”的线程长期时发生。举个例子,假设一个对象提供了一个同步方法,这个方法通常需要执行很长一段时间才返回。如果一个线程经常调用这个方法,那么其他需要同步的访问这个对象的线程就经常会被阻塞。
活锁
一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:Alphonse向他自己的左边靠想让Gaston过去,而Gaston向他的右边靠想让Alphonse过去。可见他们阻塞了对方。Alphonse向他的右边靠,而Gaston向他的左边靠,他们还是阻塞了对方。
保护块(Guarded Blocks)
(本部分 原文连接, 译文连接,译者:Greester,校对:郑旭东)
多线程之间经常需要协同工作,最常见的方式是使用GuardedBlocks,它循环检查一个条件(通常初始值为true),直到条件发生变化才跳出循环继续执行。在使用GuardedBlocks时有以下几个步骤需要注意:
假设guardedJoy()方法必须要等待另一线程为共享变量joy设值才能继续执行。那么理论上可以用一个简单的条件循环来实现,但在等待过程中guardedJoy方法不停的检查循环条件实际上是一种资源浪费。
更加高效的方法是调用Object.wait将当前线程挂起,直到有另一线程发起事件通知(尽管通知的事件不一定是当前线程等待的事件)。
注意:一定要在循环里面调用wait方法,不要想当然的认为线程唤醒后循环条件一定发生了改变。
和其他可以暂停线程执行的方法一样,wait方法会抛出InterruptedException,在上面的例子中,因为我们关心的是joy的值,所以忽略了InterruptedException。
为什么guardedJoy是synchronized方法?假设d是用来调用wait的对象,当一个线程调用d.wait,它必须要拥有d的内部锁(否则会抛出异常),获得d的内部锁的最简单方法是在一个synchronized方法里面调用wait。
当一个线程调用wait方法时,它释放锁并挂起。然后另一个线程请求并获得这个锁并调用 Object.notifyAll通知所有等待该锁的线程。
当第二个线程释放这个该锁后,第一个线程再次请求该锁,从wait方法返回并继续执行。
注意:还有另外一个通知方法,notify(),它只会唤醒一个线程。但由于它并不允许指定哪一个线程被唤醒,所以一般只在大规模并发应用(即系统有大量相似任务的线程)中使用。因为对于大规模并发应用,我们其实并不关心哪一个线程被唤醒。
现在我们使用Guardedblocks创建一个生产者/消费者应用。这类应用需要在两个线程之间共享数据:生产者生产数据,消费者使用数据。两个线程通过共享对象通信。在这里,线程协同工作的关键是:生产者发布数据之前,消费者不能够去读取数据;消费者没有读取旧数据前,生产者不能发布新数据。
在下面的例子中,数据通过 Drop对象共享的一系列文本消息:
Producer是生产者线程,发送一组消息,字符串DONE表示所有消息都已经发送完成。为了模拟现实情况,生产者线程还会在消息发送时随机的暂停。
Consumer是消费者线程,读取消息并打印出来,直到读取到字符串DONE为止。消费者线程在消息读取时也会随机的暂停。
ProducerConsumerExample是主线程,它启动生产者线程和消费者线程。
注意:Drop类是用来演示GuardedBlocks如何工作的。为了避免重新发明轮子,当你尝试创建自己的共享数据对象时,请查看 Java CollectionsFramework中已有的数据结构。如需更多信息,请参考 Questions and Exercises。
不可变对象
(本部分 原文链接, 译文链接,译者:Greenster,校对:郑旭东)
一个对象如果在创建后不能被修改,那么就称为不可变对象。在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码。
在并发编程中,不可变对象特别有用。由于创建后不能被修改,所以不会出现由于线程干扰产生的错误或是内存一致性错误。
但是程序员们通常并不热衷于使用不可变对象,因为他们担心每次创建新对象的开销。实际上这种开销常常被过分高估,而且使用不可变对象所带来的一些效率提升也抵消了这种开销。例如:使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码。
接下来看一个可变对象的类,然后转化为一个不可变对象的类。通过这个例子说明转化的原则以及使用不可变对象的好处。
一个同步类的例子
SynchronizedRGB是表示颜色的类,每一个对象代表一种颜色,使用三个整形数表示颜色的三基色,字符串表示颜色名称。
使用SynchronizedRGB时需要小心,避免其处于不一致的状态。例如一个线程执行了以下代码:
如果有另外一个线程在Statement 1之后、Statement2之前调用了color.set方法,那么myColorInt的值和myColorName的值就会不匹配。为了避免出现这样的结果,必须要像下面这样把这两条语句绑定到一块执行:
这种不一致的问题只可能发生在可变对象上。
定义不可变对象的策略
以下的一些规则是创建不可变对象的简单策略。并非所有不可变类都完全遵守这些规则,不过这不是编写这些类的程序员们粗心大意造成的,很可能的是他们有充分的理由确保这些对象在创建后不会被修改。但这需要非常复杂细致的分析,并不适用于初学者。
·不提供修改可变对象的方法。
·不共享可变对象的引用。当一个引用被当做参数传递给构造函数,而这个引用指向的是一个外部的可变对象时,一定不要保存这个引用。如果必须要保存,那么创建可变对象的拷贝,然后保存拷贝对象的引用。同样如果需要返回内部的可变对象时,不要返回可变对象本身,而是返回其拷贝。
将这一策略应用到SynchronizedRGB有以下几步:
经过以上这些修改后,我们得到了 ImmutableRGB:
高级并发对象
死锁描述了这样一种情景,两个或多个线程永久阻塞,互相等待对方释放资源。下面是一个例子。
Alphone和Gaston是朋友,都很讲究礼节。礼节有一个严格的规矩,当你向一个朋友鞠躬时,你必须保持鞠躬的姿势,直到你的朋友有机会回鞠给你。不幸的是,这个规矩没有算上两个朋友相互同时鞠躬的可能。
下面的应用例子,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(); -
} - }
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方法。
2.
饥饿和活锁并不如死锁一般普遍,但它仍然是每个并发程序设计者可能会遇到的问题。
饥饿
饥饿是指当一个线程不能正常的访问共享资源并且不能正常执行的情况。这通常在共享资源被其他“贪心”的线程长期时发生。举个例子,假设一个对象提供了一个同步方法,这个方法通常需要执行很长一段时间才返回。如果一个线程经常调用这个方法,那么其他需要同步的访问这个对象的线程就经常会被阻塞。
活锁
一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:Alphonse向他自己的左边靠想让Gaston过去,而Gaston向他的右边靠想让Alphonse过去。可见他们阻塞了对方。Alphonse向他的右边靠,而Gaston向他的左边靠,他们还是阻塞了对方。
保护块(Guarded Blocks) ![java并发教程4 Top](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
(本部分 原文连接, 译文连接,译者:Greester,校对:郑旭东)
多线程之间经常需要协同工作,最常见的方式是使用GuardedBlocks,它循环检查一个条件(通常初始值为true),直到条件发生变化才跳出循环继续执行。在使用GuardedBlocks时有以下几个步骤需要注意:
假设guardedJoy()方法必须要等待另一线程为共享变量joy设值才能继续执行。那么理论上可以用一个简单的条件循环来实现,但在等待过程中guardedJoy方法不停的检查循环条件实际上是一种资源浪费。
- public
void guardedJoy() { -
// Simple loop guard. Wastes -
// processor time. Don't do this! -
while(!joy) {} -
System.out.println("Joy has been achieved!"); - }
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!"); - }
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的值,所以忽略了InterruptedException。
为什么guardedJoy是synchronized方法?假设d是用来调用wait的对象,当一个线程调用d.wait,它必须要拥有d的内部锁(否则会抛出异常),获得d的内部锁的最简单方法是在一个synchronized方法里面调用wait。
当一个线程调用wait方法时,它释放锁并挂起。然后另一个线程请求并获得这个锁并调用 Object.notifyAll通知所有等待该锁的线程。
public synchronized notifyJoy() { joy = true; notifyAll(); }
当第二个线程释放这个该锁后,第一个线程再次请求该锁,从wait方法返回并继续执行。
注意:还有另外一个通知方法,notify(),它只会唤醒一个线程。但由于它并不允许指定哪一个线程被唤醒,所以一般只在大规模并发应用(即系统有大量相似任务的线程)中使用。因为对于大规模并发应用,我们其实并不关心哪一个线程被唤醒。
现在我们使用Guardedblocks创建一个生产者/消费者应用。这类应用需要在两个线程之间共享数据:生产者生产数据,消费者使用数据。两个线程通过共享对象通信。在这里,线程协同工作的关键是:生产者发布数据之前,消费者不能够去读取数据;消费者没有读取旧数据前,生产者不能发布新数据。
在下面的例子中,数据通过 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(); -
} - }
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"); -
} - }
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) {} -
} -
} - }
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(); -
} - }
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类是用来演示GuardedBlocks如何工作的。为了避免重新发明轮子,当你尝试创建自己的共享数据对象时,请查看 Java CollectionsFramework中已有的数据结构。如需更多信息,请参考 Questions and Exercises。
不可变对象 ![java并发教程4 Top](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
(本部分 原文链接, 译文链接,译者:Greenster,校对:郑旭东)
一个对象如果在创建后不能被修改,那么就称为不可变对象。在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码。
在并发编程中,不可变对象特别有用。由于创建后不能被修改,所以不会出现由于线程干扰产生的错误或是内存一致性错误。
但是程序员们通常并不热衷于使用不可变对象,因为他们担心每次创建新对象的开销。实际上这种开销常常被过分高估,而且使用不可变对象所带来的一些效率提升也抵消了这种开销。例如:使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码。
接下来看一个可变对象的类,然后转化为一个不可变对象的类。通过这个例子说明转化的原则以及使用不可变对象的好处。
一个同步类的例子
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; -
} - }
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<wbr>(); } } 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; } } </wbr>
使用SynchronizedRGB时需要小心,避免其处于不一致的状态。例如一个线程执行了以下代码:
- SynchronizedRGB
color = -
new SynchronizedRGB(0, 0, 0, "Pitch Black"); - ...
- int
myColorInt = color.getRGB(); //Statement 1 - String
myColorName = color.getName(); //Statement 2
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black"); ... int myColorInt = color.getRGB(); //Statement 1 String myColorName = color.getName(); //Statement 2
如果有另外一个线程在Statement 1之后、Statement2之前调用了color.set方法,那么myColorInt的值和myColorName的值就会不匹配。为了避免出现这样的结果,必须要像下面这样把这两条语句绑定到一块执行:
- synchronized
(color) { -
int myColorInt = color.getRGB(); -
String myColorName = color.getName(); - }
synchronized (color) { int myColorInt = color.getRGB(); String myColorName = color.getName(); }
这种不一致的问题只可能发生在可变对象上。
定义不可变对象的策略
以下的一些规则是创建不可变对象的简单策略。并非所有不可变类都完全遵守这些规则,不过这不是编写这些类的程序员们粗心大意造成的,很可能的是他们有充分的理由确保这些对象在创建后不会被修改。但这需要非常复杂细致的分析,并不适用于初学者。
- 不要提供setter方法。(包括修改字段的方法和修改字段引用对象的方法)
- 将类的所有字段定义为final、private的。
- 不允许子类重写方法。简单的办法是将类声明为final,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象。
- 如果类的字段是对可变对象的引用,不允许修改被引用对象。
将这一策略应用到SynchronizedRGB有以下几步:
- SynchronizedRGB类有两个setter方法。第一个set方法只是简单的为字段设值(译者注:删掉即可),第二个invert方法修改为创建一个新对象,而不是在原有对象上修改。
- 所有的字段都已经是私有的,加上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); -
} - }
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<wbr>(); } } 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); } } </wbr>
高级并发对象 ![java并发教程4 Top](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)