16.5 线程同步
多线程编程很容易突然出现“错误情况”,这是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。
16.5.1 线程安全问题
银行取钱问题
关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几个步骤
- 用户输入账户、密码,系统判断用户的账户、密码是否匹配。
- 用户输入取款金额
- 系统判断账户余额是否大于取款金额
- 如果余额大于取款金额,则取款成功;
- 如果余额小于取款金额,则取款失败
乍一看上去,这个流程确实就是日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!
程序 银行取钱问题
按上面的流程去编写取款程序,并使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。此处忽略检査账户和密码的操作,仅仅模拟后面三步操作。
账户类
下面先定义一个账户类,该账户类封装了账户编号和余额两个实例变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class Account { private String accountNo; private double balance; public Account(){} public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this == obj) return true; if (obj !=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
|
取钱线程
接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class DrawThread extends Thread { private Account account; private double drawAmount; public DrawThread(String name , Account account , double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } public void run() { if (account.getBalance() >= drawAmount) { System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } account.setBalance(account.getBalance() - drawAmount); System.out.println("\t余额为: " + account.getBalance()); } else { System.out.println(getName() + "取钱失败!余额不足!"); } } }
|
上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。
主程序
程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱。如下所示:
1 2 3 4 5 6 7 8 9 10 11
| public class DrawTest { public static void main(String[] args) { Account acct = new Account("1234567" , 1000); new DrawThread("甲" , acct , 800).start(); new DrawThread("乙" , acct , 800).start(); } }
|
运行结果
多次运行上面程序,很有可能都会看到如下所示的错误结果
1 2 3 4
| 甲取钱成功!吐出钞票:800.0 乙取钱成功!吐出钞票:800.0 余额为: -600.0 余额为: -600.0
|
账户余额出现了负值,这不是银行希望的结果。