本章主要讨论多线程程序设计时的一些问题,并发问题通常要复杂的多,容易出错,即使是高级工程师也经常会无意间犯错,然而,随着多核处理器的问世,并发几乎是程序员绕不开的一道坎。
本条又要回到大段的代码示例中。 首先,书中讲述一个观点,就是我们认为只有在写操作的时候才需要关心同步问题,实际上并非如此,读操作同样需要关心同步问题,变量的值发生改变时,其对其他线程的可见性是无法保证的。比如:
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
这个例子我们开启了一个线程,并轮询一个boolean变量来决定是否要结束线程。 读者可能会以为主线程设置boolean变量为true时,子线程就会结束。实际上,子线程永远不会结束,因为boolean变量的变更子线程没有感知到。这其中涉及到了很多JVM重排序的知识,可惜书中只是一笔带过,只是交代while语句可能会变成这样:
if(!stopRequested)
while(true)
i++;
如果将变量的读取做成同步的,如下:
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested())
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
程序便能如期望的那样工作,注意,写方法和读方法都要加同步,否则,就会不起作用。 书中还给出了更推荐的版本:
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
使用volatile
关键字,变量值的变更会立即被所有线程看到,至于内部实现机理,很遗憾,本书还是没讲。 书中提到在使用volatile
的过程中,要务必小心++
操作符是非原子的,实际上,我认为在所有并发情况下都应该要小心这个操作符是分两步执行的,即先读后写。 在这种情况下,应该使用java.util.concurrent.atomic
中的类AtomicLong
,来执行线程安全的加一操作。 书中提到,避免多线程同步问题的最佳办法是不要共享变量,或者共享不可变数据
。
本条的标题不明确,应该改成,避免不信任的外来方法进入同步代码块中。 或者改成,尽可能地,将不必要的步骤(比如不信任的外来方法)抽离同步代码块,只保留真正需要同步的代码在代码块中。
书中举例了一个观察者模式,然后给出了两种外来方法会破坏同步代码抛出异常甚至造成死锁的例子。
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) {
super(set);
}
private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
public void addObserver(SetObserver<E> observer) {
synchronized (observers) {
observers.add(observer);
}
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized (observers) {
return observers.remove(observer);
}
}
// This method is the culprit
private void notifyElementAdded(E element) {
synchronized (observers) {
for (SetObserver<E> observer : observers)
observer.added(this, element);
}
}
@Override
public boolean add(E element) {
boolean added = super.add(element);
if (added)
notifyElementAdded(element);
return added;
}
@Override
public boolean addAll(Collection<? extends E> c) {
boolean result = false;
for (E element : c)
result |= add(element); // calls notifyElementAdded
return result;
}
}
在上面的例子中,由于在同步块内调用了外来者observer的added方法,该方法完全由外来者observer来定义,也许在未来的某一刻,它就会修改为作者给出的两个不安全的版本,从而导致问题:
public class Test2 {
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<Integer>(
new HashSet<Integer>());
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
}
上面这个版本会导致ConcurrentModificationException,因为第10行的回调过程实际上要从列表里删除观察者,而此时,该列表正在被遍历。 (我觉得作者提出的这个例子来证明外来类的不安全性是有问题的,因为即使在单线程的程序中,回调方法依然有这样的安全隐患,和同步与否没什么必然联系)
public class Test3 {
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<Integer>(
new HashSet<Integer>());
// Observer that uses a background thread needlessly
set.addObserver(new SetObserver<Integer>() {
public void added(final ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) {
ExecutorService executor = Executors
.newSingleThreadExecutor();
final SetObserver<Integer> observer = this;
try {
executor.submit(new Runnable() {
public void run() {
s.removeObserver(observer);
}
}).get();
} catch (ExecutionException ex) {
throw new AssertionError(ex.getCause());
} catch (InterruptedException ex) {
throw new AssertionError(ex.getCause());
} finally {
executor.shutdown();
}
}
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
}
上面这个版本会导致死锁,主要问题在于removeObserver方法是个同步方法,会尝试获得锁,而我们的主线程已经拥有该锁了,于是子线程在等待获得锁,主线程在等待子线程结束,导致程序死锁,死在某个地方。 值得注意的是,作者很偷鸡地在executor.submit方法之后调用了get方法,其实,如果没有多余地调用这个方法,例子是完全不会死锁的,主线程在一般场景下,没理由等待子线程结束。