线程的并发问题(一)
文章来源:《Head First Java》修炼感悟。
尽管多线程设计很令人兴奋,但还是要尽可能「避坑」。 想象一下,当你用精心设计的两个线程同时维护某个对象数据时,这个数据安全吗? 这就是本文要讨论的问题 - 线程的并发性可能会破坏数据安全。
一、情侣的公共账户
现实生活当中可能会发生这样的事情:热恋中的花花和草草同时入职名企,兴奋之余对未来做起了规划。 两人约定每月工资都存进同一个账户,不限制花销,但绝对不允许透支。 可以保证的是,二人都非常守信。但这样真的可以吗? 如果两人事先没有沟通,这个账户还是可能会出现透支情况。
我们用代码演绎一下这个情节:
java">// 账户操作类
public class JobRunnable implements Runnable {// 新建一个公共账户private BankAcount acount = new BankAcount();public void run() {// 取钱操作,每次取出 10 元钱// 如果账户余额小于零,即表示账户透支for(int i = 0; i < 10; i++) {withdrawal(10); if (acount.getBalance() < 0) {System.out.println("Overdrawn!");}}}// 创建两个线程,同时提取账户余额public static void main(String[] args) {JobRunnable job = new JobRunnable();Thread huahua = new Thread(job);Thread caocao = new Thread(job);huahua.setName("HuaHua");caocao.setName("CaoCao");huahua.start();caocao.start();}// 从账户中提取指定金额。// 正常情况下,余额不足时看起来绝对不会执行的 acount.withdrawal()// 方法也被执行了,这是因为其中一个线程查询了余额后,同时另一个线程// 做了提取操作,线程间并没有得到更新后的余额,所以导致对账户余额判断// 出现错误,这就是典型的线程并发性导致的问题。private void withdrawal(int amount) {// 账户余额不足,直接返回if (acount.getBalance() < amount) {System.out.println("Sorry, not enough for " + Thread.currentThread().getName());return;}// 准备取钱System.out.println(Thread.currentThread().getName() + " is about to withdraw.");try {System.out.println(Thread.currentThread().getName() + " is going to sleep.");Thread.sleep(500); // 让线程小睡一会儿} catch (InterruptedException e) {e.printStackTrace();}// 线程被唤醒,开始取钱System.out.println(Thread.currentThread().getName() + " woke up.");acount.withdrawal(amount);// 交易结束System.out.println(Thread.currentThread().getName() + " completes the withdrawal.");}
}
// 账户类
class BankAcount {private int balance = 100; // 账户余额public int getBalance() {return balance;}// 取出指定的钱数并更新余额public void withdrawal(int amount) {balance -= amount;}
}
运行这段代码:
不出意外,这个账户在不经意间透支了,下个月可咋过? 现在我们来梳理一下为什么会出现这个问题:
- 花花准备从账户中取钱,查询后发现账户余额足够,就放心地睡觉去了;
- 草草也想从账户中取钱,也查询了账户余额足够,也睡了一会儿;
- 花花醒来后直接从账户中取走了所需的钱;
- 很快草草也睡醒了,他记得余额足够所以直接取走了他所需的钱;
- 结果,两人在都不知情的情况下,账户透支了。
其实两人想法是好的,但缺少了保护机制:在账户取钱的过程中,不应该允许其他人在同一时时操作该账户,除非前面的交易已经完成。
二、为账户加上同步锁
我们再来分析一下应该怎样保护好这个账户:
- 为账户设置一道锁和一把钥匙,平时锁处于开放状态谁都可以操作;
- 当操作账户时,锁住账户并收好钥匙,确保除了自己谁也无法操作该账户;
- 账户操作结束后,打开账户锁并归还钥匙,确保下次可以正常操作。
我们只要锁住账户存取的方法就可以,请看代码:
java">// 从账户中取钱,这次有 synchronized 锁
private synchronized void withdrawal(int amount) {if (acount.getBalance() < amount) {System.out.println("Sorry, not enough for " + Thread.currentThread().getName());return;}// 其它代码...
}
注意,withdrawal()
方法使用了 synchronized
关键字修饰,表示同步的意思,它的作用就是保证同一时间只能有一个线程执行该方法。 我们来看看加上了同步锁后的效果:
可以了,这次账户不会透支了!
《 上一篇 线程的调度机制 |
---|