foundationReady_ = true;
}
}
public class BrickLayer extends Thread {
public void run() {
House a = getHouse();
while (!a.foundationReady_) {
try {
Thread.sleep(500);
}
catch (Exception e) {
System.err.println(“Exception:” + e);
}
}
}
}
}
尽管存在竞争,但根据Java VM规范,Boolean数据的读取和写入都是原则性的,也就是说,VM不能中断线程的读取或写入操作。一旦数据改动成功,不存在将它改回原来数据的必要(不需要“回退”),所以代码三的数据竞争是良性竞争,代码是安全的。
恶性数据竞争
首先看一下代码四的例子。
//代码四
public class Account {
private int balance_; // 账户余额
public int getBalance(void) {
return balance_;
}
public void setBalance(int setting) {
balance_ = setting;
}
}
public class CustomerInfo {
private int numAccounts_;
private Account accounts_;
public void withdraw(int accountNumber, int amount) {
int temp = accounts_[accountNumber].getBalance();
temp = temp - amount;
accounts_[accountNumber].setBalance(temp);
}
public void deposit(int accountNumber, int amount) {
int temp = accounts_[accountNumber].getBalance();
temp = temp + amount;
accounts_[accountNumber].setBalance(temp);
}
}
如果丈夫A和妻子B试图通过不同的银行柜员机同时向同一账户存钱,会发生什么事情?让我们假设账户的初始余额是100元,看看程序的一种可能的执行经过。
B存钱25元,她的柜员机开始执行deposit()。首先取得当前余额100,把这个余额保存在本地的临时变量,然后把临时变量加25,临时变量的值变成125。现在,在调用setBalance()之前,线程调度器中断了该线程。
A存入50元。当B的线程仍处于挂起状态时,A这面开始执行deposit():getBalance()返回100(因为这时B的线程尚未把修改后的余额写入),A的线程在现有余额的基础上加50得到150,并把150这个值保存到临时变量。接着,A的线程在调用setBalance()之前,也被中断执行。
现在,B的线程接着运行,把保存在临时变量中的值(125)写入到余额,柜员机告诉B说交易完成,账户余额是125元。接下来,A的线程继续运行,把临时变量的值(150)写入到余额,柜员机告诉A说
交易完成,账户余额是150元。
最后得到的结果是什么?B的存款消失不见,就像B根本没有存过钱一样。
也许有人会认为,可以把getBalance()和setBalance()改成同步方法保护Account.balance_,解决数据竞争问题。其实这种办法是行不通的。“synchronized”关键词可以确保同一时刻只有一个线程执行getBalance()或setBalance()方法,但这不能在一个线程操作期间阻止另一个线程修改账户余额。
要正确运用“synchronized”关键词,就必须认识到这里要保护的是整个
交易过程不被另一个线程干扰,而不仅仅是对数据访问的某一个步骤进行保护。
所以,本例的关键是当一个线程获得当前余额之后,要保证其它的线程不能修改余额,直到第一个线程的余额处理工作全部完成。正确的修改方法是把deposit()和withdraw()改成同步方法。
死锁、隐性死锁和数据竞争是Java多线程编程中最常见的错误。要写出健壮的多线程代码,正确理解和运用“synchronized”关键词是很重要的。另外,好的线程分析工具,例如