摘要:只要加两把锁,就有可能造成死锁。
尤其危险的是对于被继承或者实现的方法加锁。
You should be forgiven if, after reading the preceding text, you thought that the only way to be safe in a multithreaded world was to make every method synchronized. Unfortunately, it’s not that easy.
Firstly, this would be dreadfully inefficient. If every method were synchronized, most threads would probably spend most of their time blocked, defeating the point of making your code concurrent in the first place. But this is the least of your worries - as soon as you have more than one lock(remember, in Java every object has its own lock), you create the opportunity for threads to become deadlocked.
We’ll demonstrate deadlock with a nice little example commonly used in academic papers on concurrency - the “dining philosophers” problem. Imagine that five philosophers are sitting around a table, with five(not ten) chopsticks.
A philosopher is either thinking or hungry. If he’s hungry, he picks up the chopsticks on either side of him and eats for a while(yes, our philosophers are male-women would behave more sensibly). When he’s done, he puts them down.
Here’s how we might implement one of our philosophers:
public class Philosopher extends Thread{
static class Chopstick{
}
private Chopstick mLeft, mRight;
private Random mRandom;
public Philosopher(Chopstick left, Chopstick right){
mLeft = left;
mRight = right;
mRandom = new Random();
}
public void run() {
try {
while (true) {
Thread.sleep(mRandom.nextInt(1000));
synchronized (mLeft) {
synchronized (mRight) {
Thread.sleep(mRandom.nextInt(1000));
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
On my machine, if I set five of these going simultaneously, they typically run very happily for hours on end(my record is over a week). Then, all of a sudden, everything grinds to a halt.
After a little thought, it’s obvious what’s going on - if all the philosophers decided to eat at the same time, they all grab their left chopstick and then find themselves stuck - eash has one chopstick, and each is blocked waiting for the philosopher on his right. Deadlock.
Deadlock is a danger whenever a thread tries to hold more than one lock. Happily, there is a simple rule that guarantees you will never deadlock - always acquire locks in a fixed , global order.
Here’s one way we can achieve this:
import java.util.Random;
public class PhilosopherSafe extends Thread{
static class Chopstick{
private int mId;
Chopstick(int mId) {
this.mId = mId;
}
public int getId() {
return mId;
}
}
private Chopstick mFirst, mSecond;
private Random mRandom;
public PhilosopherSafe(Chopstick left, Chopstick right){
if(left.getId() < right.getId()){
mFirst = left;
mSecond = right;
}else{
mFirst = right;
mSecond = left;
}
mRandom = new Random();
}
public void run() {
try {
while (true) {
Thread.sleep(mRandom.nextInt(1000));
synchronized (mFirst) {
synchronized (mSecond) {
Thread.sleep(mRandom.nextInt(1000));
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Instead of holding on to left and right chopsticks, we now hold on to first and second, using Chopstick’s id member to ensure that we always lock chopsticks in increasing ID order(we don’t actually care what IDs chopsticks have - just that they’re unique and ordered). And sure enough, now things will happily run forever without locking up.
It’s easy to see how to stick to the global ordering rule when the code to acquire locks is all in one place. It gets much harder in a large program, where a global understanding of what all the code is doing is impractical.
Large programs often make use of listeners to decouple modules. Here, for example, is a class that downloads from a URL and allows ProgressListeners to be registered
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
public class Downloader extends Thread {
public static interface ProgressListener {
void onProgress(int n);
}
private InputStream mIn;
private OutputStream mOut;
private ArrayList<ProgressListener> mListeners;
public Downloader(URL url, String outputFilename) throws IOException {
mIn = url.openConnection().getInputStream();
mOut = new FileOutputStream(outputFilename);
mListeners = new ArrayList<>();
}
public synchronized void addListener(ProgressListener listener) {
mListeners.add(listener);
}
public synchronized void removeListener(ProgressListener listener) {
mListeners.remove(listener);
}
private synchronized void updateProgress(int n) {
for (ProgressListener listener : mListeners)
listener.onProgress(n);
}
public void run() {
int n = 0, total = 0;
byte[] buffer = new byte[1024];
try{
while((n=mIn.read(buffer))!= -1){
mOut.write(buffer,0,n);
total += n;
updateProgress(n);
}
mOut.flush();
}catch(IOException e){
}
}
}
Because addListener(), removeListener() and updateProgress() are all synchronized, multiple threads can call them without stepping on one another’s toes. But a trap lurks in this code that could lead to deadlock even though there’s only a single lock in use.
The problem is that updateProgress() calls an alien method - a method it knows nothing about. That method could do anything, including acquiring another lock. If it does, then we’ve acquired two locks without knowing whether we’ve done so in the right order. As we’ve just seen, that can lead to deadlock.
The only solution is to avoid calling alien methods while holding a lock. One way to achieve this is to make a defensive copy of listeners before iterating through it:
private void updateProgressSafe(int n){
ArrayList<ProgressListener> listenersCopy;
synchronized (this){
listenersCopy = (ArrayList<ProgressListener>) mListeners.clone();
}
for(ProgressListener listener : listenersCopy){
listener.onProgress(n);
}
}
This changes kills several birds with one stone. Not only does it avoid calling an alien method with a lock held, it also minimizes the period during which we hold the lock. Holding locks for longer than necessary both hurts performance(by restricting the degree of achievable concurrency) and increases the danger of deadlock. This change also fixes another bug that isn’t related to concurrency - a listener can now call removeListen() with its onProgress() method without modifying the copy of listeners that’s mid-iteration.