线程的同步

多线程编程时有趣的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在
运行过程中偶尔出现问题,那也是由于我们编程不当引起的。当使用多个线程来访问同一个数据时,非常容易出现线程安全

问题。

线程安全问题

关于线程安全的问题,有一个经典的问题:银行取钱的问题。银行取钱的基本流程基本可以分为如下几个步骤:
1、用户输入账户、密码,系统判断用户的账户、密码是否匹配。
2、用户输入取款金额。
3、系统判断账户余额是否大于取款金额。
4、根据以上判断,要么取款成功,要么失败。
咋一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程并没有任何问题。但一旦将这个流程放在多线程
并发的场景下,就有可能出现问题。我们按照上面的流畅去编写取款程序,而且我们使用两条线程来模拟取钱操作,模拟
两个人使用同一个账户并发取钱的问题。当然我们不管检查账户和密码的操作,仅仅模拟了后面的三部操作。下面先定义了
一个账户类,该账户类封装了账户编号和余额两个属性。

[java]  view plain copy
  1. public class Account {  
  2.     /** 封装账户编号、账户余额两个属性 */  
  3.     private String accountNo;  
  4.     private double balance;  
  5.   
  6.     public Account() {  
  7.   
  8.     }  
  9.   
  10.     public Account(String accountNo, double balance) {  
  11.         this.accountNo = accountNo;  
  12.         this.balance = balance;  
  13.     }  
  14.       
  15.     public String getAccountNo() {  
  16.         return accountNo;  
  17.     }  
  18.   
  19.     public void setAccountNo(String accountNo) {  
  20.         this.accountNo = accountNo;  
  21.     }  
  22.   
  23.     public double getBalance() {  
  24.         return balance;  
  25.     }  
  26.   
  27.     public void setBalance(double balance) {  
  28.         this.balance = balance;  
  29.     }  
  30.   
  31.     //下面两个方法根据accountNo来计算Account的hashCode和判断equals  
  32.     public int hashCode(){  
  33.         return accountNo.hashCode();  
  34.     }  
  35.     public boolean equals(Object obj){  
  36.         if(obj != null && obj.getClass() == Account.class){  
  37.             Account target = (Account)obj;  
  38.             return target.getAccountNo().equals(accountNo);  
  39.         }  
  40.         return false;  
  41.     }  
  42. }  



[java]  view plain copy
  1. public class DrawThread extends Thread{  
  2.     /** 模拟用户账户 */  
  3.     private Account account;  
  4.     /**当前取钱线程 所希望取的钱数 */  
  5.     public double drawAmount;  
  6.     public DrawThread(String name,Account account,double drawAmount){  
  7.         super(name);  
  8.         this.account = account;  
  9.         this.drawAmount = drawAmount;  
  10.     }  
  11.     /** 
  12.      * 当多条线程修改同一个共享数据时,将涉及数据安全问题 
  13.      */  
  14.     @Override  
  15.     public void run() {  
  16.         super.run();  
  17.           
  18.         if(account.getBalance() >= drawAmount){  
  19.             //吐出钞票  
  20.             System.out.println(getName() + "取钱成功!吐出钞票" + drawAmount);  
  21.             //这一段代码先注释掉  
  22. /*          try { 
  23.                 Thread.sleep(1); 
  24.             } catch (InterruptedException e) { 
  25.                 e.printStackTrace(); 
  26.             }*/  
  27.             //修改余额  
  28.             account.setBalance(account.getBalance() - drawAmount);  
  29.             System.out.println("\t余额为:" + account.getBalance());  
  30.         }else{  
  31.             System.out.println(getName() + "取钱失败!余额不足");  
  32.         }  
  33.           
  34.           
  35.     }  
  36. }  

[java]  view plain copy
  1. public static void main(String[] args) {  
  2.     //创建一个账户  
  3.     Account account = new Account("123"1000);  
  4.     //模拟两个线程对同一个账户取钱  
  5.     new DrawThread("甲", account, 800).start();  
  6.     new DrawThread("乙", account, 800).start();  
  7.   
  8. }  

可以看到取消DrawThread类注释掉的那段代码和未取消注释的运行结果完全不同,并且跟现实情况不相符。



同步代码块

之所以会出现如上图片的情况,其语音是因为run方法的方法体不具有同步安全性:程序中有两条并发线程在修改Account
对象,而且系统恰好在粗体字代码处执行线程切换,切换给另一条线程修改Account对象,所以就出现了问题。 
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块
。同步代码块的用法格式如下:
synchronized(obj){
...
//此处的代码就是同步代码块


上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先
获得对同步监视器的锁定。 
注意:任何时候只有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步
监视器的锁定。 
虽然Java程序允许使用任何对象来作为同步监视器,但想以下同步监视器的目的:阻止两条以上的线程对同一个共享资源
进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑
使用账户(Account)作为同步监视器。我们只需要把程序修改如下

[java]  view plain copy
  1. public class DrawThread extends Thread {  
  2.     /** 模拟用户账户 */  
  3.     private Account account;  
  4.     /** 当前取钱线程 所希望取的钱数 */  
  5.     public double drawAmount;  
  6.   
  7.     public DrawThread(String name, Account account, double drawAmount) {  
  8.         super(name);  
  9.         this.account = account;  
  10.         this.drawAmount = drawAmount;  
  11.     }  
  12.   
  13.     /** 
  14.      * 当多条线程修改同一个共享数据时,将涉及数据安全问题 
  15.      */  
  16.     @Override  
  17.     public void run() {  
  18.         super.run();  
  19.         /** 
  20.          * 使用Account作为同步监视器,热河线程进入下面同步代码块之前,必须先获得对account账户的锁定,其他线程 
  21.          * 无法获得锁,也就无法修改它 这种做法逻辑符合:加锁--修改完成--释放锁 逻辑 
  22.          */  
  23.         synchronized (account) {  
  24.   
  25.             if (account.getBalance() >= drawAmount) {  
  26.                 // 吐出钞票  
  27.                 System.out.println(getName() + "取钱成功!吐出钞票" + drawAmount);  
  28.                 // 这一段代码先注释掉  
  29.                 /* 
  30.                  * try { Thread.sleep(1); } catch (InterruptedException e) { 
  31.                  * e.printStackTrace(); } 
  32.                  */  
  33.                 // 修改余额  
  34.                 account.setBalance(account.getBalance() - drawAmount);  
  35.                 System.out.println("\t余额为:" + account.getBalance());  
  36.             } else {  
  37.                 System.out.println(getName() + "取钱失败!余额不足");  
  38.             }  
  39.         }// 同步代码块结束,该线程释放同步锁  
  40.   
  41.     }  
  42. }  

同步方法

与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个
方法,则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是
该对象本身。
通过使用同步方法可以非常方便的将某类变成线程安全的类,线程安全的类具有如下特征:
== 该类的对象可以被多个线程安全的访问
== 每个线程调用该对象的任意方法之后都将得到正确结果。
== 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态 

我们知道有可变类和不可变类,其中不可变类总是线程安全的,因为它的对象的状态不可改变。但可变对象需要额外的
方法来保证其线程安全。例如上面的Account就是一个可变类,它的account和balance两个属性都可变,当两个线程同时
修改Account对象的balance属性时,程序就出现了异常。下面我们将Account类对balance的访问设置成线程安全的,那么
程序只要把修改的balance的方法修改成同步方法即可。程序如下:
[java]  view plain copy
  1. public class Account {  
  2.     /** 封装账户编号、账户余额两个属性 */  
  3.     private String accountNo;  
  4.     private double balance;  
  5.   
  6.     public Account() {  
  7.   
  8.     }  
  9.   
  10.     public Account(String accountNo, double balance) {  
  11.         this.accountNo = accountNo;  
  12.         this.balance = balance;  
  13.     }  
  14.   
  15.     public String getAccountNo() {  
  16.         return accountNo;  
  17.     }  
  18.   
  19.     public void setAccountNo(String accountNo) {  
  20.         this.accountNo = accountNo;  
  21.     }  
  22.   
  23.     // 因为账户余额不允许随便修改,所以取消balance属性的setter方法  
  24.     public double getBalance() {  
  25.         return balance;  
  26.     }  
  27.   
  28.     // 下面两个方法根据accountNo来计算Account的hashCode和判断equals  
  29.     public int hashCode() {  
  30.         return accountNo.hashCode();  
  31.     }  
  32.   
  33.     public boolean equals(Object obj) {  
  34.         if (obj != null && obj.getClass() == Account1.class) {  
  35.             Account1 target = (Account1) obj;  
  36.             return target.getAccountNo().equals(accountNo);  
  37.         }  
  38.         return false;  
  39.     }  
  40.   
  41.     // 提供一个线程安全的draw方法来完成取钱操作  
  42.     public synchronized void draw(double drawAmount) {  
  43.         // 账户余额大于取钱数目  
  44.         if (balance >= drawAmount) {  
  45.             // 吐出钞票  
  46.             System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票"  
  47.                     + drawAmount);  
  48.             try {  
  49.                 Thread.sleep(1);  
  50.             } catch (InterruptedException e) {  
  51.                 e.printStackTrace();  
  52.             }  
  53.             // 修改余额  
  54.             balance -= drawAmount;  
  55.             System.out.println("\t余额为:" + balance);  
  56.         } else {  
  57.             System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足");  
  58.         }  
  59.     }  
  60.   
  61. }  
上面程序中增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰修改方法,把该方法编程同步方法。同步
方法的监视器是this,因此对于同一个账户Account而言,任何时刻只能有一条线程获得对Account对象的锁定,然后进入draw
方法执行取钱操作--这样可以保证多条线程并发取钱的线程安全。

注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。

因为Account类中已经提供了draw方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该类只要调用
Account对象的draw方法来执行取钱操作。程序如下:
[java]  view plain copy
  1. public class DrawThread extends Thread{  
  2.     /** 模拟用户账户 */  
  3.     private Account account;  
  4.     /** 当前取钱线程 所希望取的钱数 */  
  5.     public double drawAmount;  
  6.   
  7.     public DrawThread(String name, Account account, double drawAmount) {  
  8.         super(name);  
  9.         this.account = account;  
  10.         this.drawAmount = drawAmount;  
  11.     }  
  12.     @Override  
  13.     public void run() {  
  14.         super.run();  
  15.         account.draw(drawAmount);  
  16.     }  
  17. }  


注意:此时的程序把draw方法定义在Account里,而不是直接在run方法中实现取钱逻辑,这种做法更符合面向对象规则。

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
== 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源 (竞争资源也就是共享资源)的方法进行同步
。例如上面的Account类中accountNo属性无须同步,所以程序只对draw方法进行同步控制。
== 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程
安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境下使用线程安全版本。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值