当使用多个线程来访问同一个数据时,就容易出现线程安全的问题。例如,银行取钱。当我们去自动取款机取钱时,正好另一个人转账,即多个线程修改同一数据,这时就容易出现线程安全问题。
线程安全
- /**
- * 账户类,该类封装了账户编号和余额两个属性
- * @author Emily-T
- *
- */
- public class Account {
- //账户编号
- private String accountNo;
- //余额
- private double balance;
- public Account(){}
- //构造函数
- public Account(String accountNo,double balance){
- this.accountNo = accountNo;
- this.balance = balance;
- }
- //下面两个方法根据accountNo来计算Account的hashCode和判断equals
- public int hashCode(){
- return accountNo.hashCode();
- }
- public boolean equals(Object obj){
- if (obj != null && obj.getClass() == Account.class) {
- Account target = (Account) obj;
- return target.getAccountNo().equals(accountNo);
- }
- return false;
- }
- public String getAccountNo() {
- return accountNo;
- }
- public void setAccountNo(String accountNo) {
- this.accountNo = accountNo;
- }
- public double getBalance() {
- return balance;
- }
- public void setBalance(double balance) {
- this.balance = balance;
- }
- }
- /**
- * 取钱的线程类
- *
- * @author Emily-T
- *
- */
- 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("取钱成功!吐出钞票:" + drawAmount);
- // try {
- // Thread.sleep(1);
- // } catch (InterruptedException e) {
- // e.printStackTrace();
- // }
- // 修改余额
- account.setBalance(account.getBalance() - drawAmount);
- System.out.println("\t余额为:" + account.getBalance());
- } else {
- System.out.println(getName() + "取钱失败!余额不足!");
- }
- }
- }
- /**
- * 启动两个线程
- * @author Emily-T
- *
- */
- public class TestDraw {
- public static void main(String[] args){
- //创建一个账户
- Account acct = new Account("1234567",1000);
- //模拟两个线程对同一个账户取钱
- new DrawThread("甲",acct,800).start();
- new DrawThread("乙",acct,800).start();
- }
- }
从结果看来,账户余额只有1000,取出了1600元,剩下-200元。出现这种结果是因为run方法的方法体不具有同步安全性,程序中有两条并发线程在修改Account对象。
线程同步
修改如下:加上同步代码块:
- // 当多条线程修改同一个共享数据时,将涉及数据安全问题
- public void run() {
- // 使用account作为同步监视器,任何线程进入下面同步代码块之前,必须先获得
- // 对account账户的锁定——其他线程无法获得锁,也就是无法修改它
- // 加锁——修改完成——释放锁
- synchronized (account) {
- // 账户余额大于取钱数目
- if (account.getBalance() >= drawAmount) {
- // 吐出钞票
- System.out.println("取钱成功!吐出钞票:" + drawAmount);
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 修改余额
- account.setBalance(account.getBalance() - drawAmount);
- System.out.println("\t余额为:" + account.getBalance());
- } else {
- System.out.println(getName() + "取钱失败!余额不足!");
- }
- }
- }
任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。
同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
可变类的线程安全是以降低程序的运行效率为代价的,为了减少程序安全所带来的负面影响,程序可以采用如下策略:
1、不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步
2、如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。