竞态条件
竞态条件:代码行为取决于各操作的时序。
public class Counting {
static class Counter {
private int count = 0;
public void increment() {
++count;
}
public int getCount() {
return count;
}
}
final Counter counter = new Counter();
class CountingThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
}
}
public static void main(String[] args) throws InterruptedException {
Counting counting = new Counting();
CountingThread t1 = counting.new CountingThread();
CountingThread t2 = counting.new CountingThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counting.counter.getCount());
}
}
运行上述代码多次,结果不是每次都为20000,原因是两个线程使用counter.count
对象时发生了竞态条件。
分析:++count
字节码如下:
getfield #2 //获取count的值
iconst_1 //常数1
iadd //count+1
putfield #2 //更新的值写回count
上述操作通常称为读-改-写(read-modify-write)模式
假如两个线程同时调用increment(),线程1执行getfield #2
获得值42,在线程1执行其他动作之前,线程2也执行了getfield #2
获得值42,如此两个线程都将获得值加1写回count中,结果count只被递增了一次,而不是两次。
解决:竞态条件的解决方案是对count进行同步(synchronize)访问,如下:
static class Counter {
//...
public synchronized void increment() {
++count;
}
//...
}
线程进入increment()函数时,将获取Counter对象级别的锁,函数返回时释放该锁。某一时间至多有一个线程可以执行函数体,其他线程调用函数时将被阻塞直到锁被释放。
使用
java.util.concurrent.atomic
包更好
内存可见性
public class Puzzle {
static boolean answerReady = false;
static int answer = 0;
static Thread t1 = new Thread() {
@Override
public void run() {
answer = 42;
answerReady = true;
}
};
static Thread t2 = new Thread() {
@Override
public void run() {
if (answerReady) {
System.out.println("The answer is: " + answer);
} else {
System.out.println("The answer is not ready");
}
}
};
public static void main(String[] args) throws InterruptedException {
t1.start();
t2.start();
t1.join();
t2.join();
}
}
上述可能的结果有The answer is: 42
或者The answer is not ready
,还有可能The answer is: 0
- 编译器的静态优化可以打乱代码的执行顺序;
- JVM的动态优化也可以打乱代码的执行顺序;
- 硬件可以通过打乱代码的执行来优化其性能;
比乱序执行更糟糕的是,有时候一个线程产生的修改可能对另一个线程不可见,如下:
public void run() {
while (!answerReady){
Thread.sleep(100);
System.out.println("The answer is: "+answer);
}
}
可能answerReady
不会变为true,线程2不会退出。
内存可见性基本原则是,如果读线程和写线程不进行同步,就不能保证可见性。
同步的方法有:
- 获取对象的内置锁;
- 线程join();
- 使用
java.util.concurrent
包提供的工具;
很容易忽略的一个重点是两个线程都需要进行同步,上述竞态条件例子,只在写increment()
添加了内置锁,而读getCount()
未进行同步,然而例子是线程安全的,因为是在join()
之后调用的getCount()
,但是为其他调用getCount()
的代码埋下了隐患,可能会读取到失效的值,安全科学的做法是getCount()
添加同步。