首页 > 代码库 > 多线程总结四:线程同步(一)

多线程总结四:线程同步(一)

1、线程安全问题

a、银行取钱问题:取钱时银行系统判断账户余额是否大于取款金额,如果是,吐出钞票,修改余额。这个流程在多线程并发的场景下就可能会出现问题。

 1 /** 2  * @Title: Account.java  3  * @Package   4  * @author 任伟 5  * @date 2014-12-8 下午5:35:27  6  * @version V1.0   7  */ 8  9 /**10  * @ClassName: Account11  * @Description: 账户类12  * @author 任伟13  * @date 2014-12-8 下午5:35:2714  */15 public class Account {16     private String accountNo; // 账户编号17     private double balance; // 账户余额18 19     public String getAccountNo() {20         return accountNo;21     }22 23     public void setAccountNo(String accountNo) {24         this.accountNo = accountNo;25     }26 27     public double getBalance() {28         return balance;29     }30 31     public void setBalance(double balance) {32         this.balance = balance;33     }34 35     public Account() {36         super();37     }38 39     public Account(String accountNo, double balance) {40         super();41         this.accountNo = accountNo;42         this.balance = balance;43     }44 45     public boolean equals(Object anObject) {46         if(this==anObject)47             return true;48         if(anObject!=null && anObject.getClass()==Account.class){49             Account target = (Account) anObject;50             return target.getAccountNo().equals(accountNo);51         }52         return false;53     }54 55     public int hashCode() {56         return accountNo.hashCode();57     }58 59 }
Account
 1 /** 2  * @Title: DrawThread.java  3  * @Package   4  * @author 任伟 5  * @date 2014-12-8 下午5:41:46  6  * @version V1.0   7  */ 8  9 /**10  * @ClassName: DrawThread11  * @Description: 取钱类12  * @author 任伟13  * @date 2014-12-8 下午5:41:4614  */15 public class DrawThread extends Thread {16     private Account account; // 用户帐户17     private double drawAmount; // 希望取的钱数18 19     public DrawThread(String name, Account account, double drawAmount) {20         super(name);21         this.account = account;22         this.drawAmount = drawAmount;23     }24     25     //当多个线程修改同一共享数据时,将涉及线程安全问题26     /* (non-Javadoc)27      * @see java.lang.Thread#run()28      */29     @Override30     public void run() {31         if(account.getBalance()>=drawAmount){32             System.out.println(this.getName()+"取钱成功,吐出钞票:"+drawAmount);33             try {34                 Thread.sleep(1);35             } catch (InterruptedException e) {36                 // TODO Auto-generated catch block37                 e.printStackTrace();38             }39             account.setBalance(account.getBalance()-drawAmount);40             System.out.println("余额为:"+account.getBalance());41         }else{42             System.out.println(this.getName()+"取钱失败!余额不足!");43         }44     }45     46     public static void main(String[] args) {47         //创建一个账户48         Account acct = new Account("1234567", 1000);49         //模拟两个线程对同一个账户取钱50         new DrawThread("甲", acct, 800).start();51         new DrawThread("乙", acct, 800).start();52     }53 54 }
DrawThread

运行结果:

2、同步代码块

a、Java多线程引入了同步监视器来解决这个问题:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,在同步代码块执行完以后,该线程会释放对该同步监视器的锁定。通常使用可能被并发访问的共享资源充当同步监视器。

 1 synchronized(obj){//obj同步监视器 2 //同步代码块 3 } 

 1 /** 2  * @Title: SynchronizedBlockDrawThread.java  3  * @Package   4  * @author 任伟 5  * @date 2014-12-8 下午5:59:06  6  * @version V1.0   7  */ 8  9 /**10  * @ClassName: SynchronizedBlockDrawThread11  * @Description: 使用Account作为同步监视器12  * @author 任伟13  * @date 2014-12-8 下午5:59:0614  */15 public class SynchronizedBlockDrawThread extends Thread {16     private Account account; // 用户帐户17     private double drawAmount; // 希望取的钱数18 19     public SynchronizedBlockDrawThread(String name, Account account,20             double drawAmount) {21         super(name);22         this.account = account;23         this.drawAmount = drawAmount;24     }25 26     // 当多个线程修改同一共享数据时,将涉及线程安全问题27     /*28      * (non-Javadoc)29      * 30      * @see java.lang.Thread#run()31      */32     @Override33     public void run() {34         synchronized (account) {35             if (account.getBalance() >= drawAmount) {36                 System.out.println(this.getName() + "取钱成功,吐出钞票:" + drawAmount);37                 try {38                     Thread.sleep(1);39                 } catch (InterruptedException e) {40                     // TODO Auto-generated catch block41                     e.printStackTrace();42                 }43                 account.setBalance(account.getBalance() - drawAmount);44                 System.out.println("余额为:" + account.getBalance());45             } else {46                 System.out.println(this.getName() + "取钱失败!余额不足!");47             }48         }49     }50 51     public static void main(String[] args) {52         // 创建一个账户53         Account acct = new Account("1234567", 1000);54         // 模拟两个线程对同一个账户取钱55         new SynchronizedBlockDrawThread("甲", acct, 800).start();56         new SynchronizedBlockDrawThread("乙", acct, 800).start();57     }58 59 }
SynchronizedBlockDrawThread

运行结果:

 

3、同步方法
a、使用synchronized关键字来修饰某一个方法,使用this作为同步监视器,也就是调用该方法的对象。
将Account变为线程安全的类:

 1 /** 2  * @Title: Account2.java  3  * @Package   4  * @author 任伟 5  * @date 2014-12-8 下午6:50:34  6  * @version V1.0   7  */ 8  9 /** 10  * @ClassName: Account2 11  * @Description: 线程安全Account类12  * @author 任伟13  * @date 2014-12-8 下午6:50:34  14  */15 public class Account2 {16     private String accountNo; // 账户编号17     private double balance; // 账户余额18     19     public Account2() {20         super();21     }22 23     public Account2(String accountNo, double balance) {24         super();25         this.accountNo = accountNo;26         this.balance = balance;27     }28 29     public boolean equals(Object anObject) {30         if(this==anObject)31             return true;32         if(anObject!=null && anObject.getClass()==Account.class){33             Account target = (Account) anObject;34             return target.getAccountNo().equals(accountNo);35         }36         return false;37     }38 39     public int hashCode() {40         return accountNo.hashCode();41     }42     43     public synchronized void draw(double drawAmount){44         if (balance >= drawAmount) {45             System.out.println(Thread.currentThread().getName() + "取钱成功,吐出钞票:" + drawAmount);46             try {47                 Thread.sleep(1);48             } catch (InterruptedException e) {49                 // TODO Auto-generated catch block50                 e.printStackTrace();51             }52             balance -= drawAmount;53             System.out.println("余额为:" + balance);54         } else {55             System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!");56         }57     }58 }
Account2
 1 /** 2  * @Title: DrawThread2.java  3  * @Package   4  * @author 任伟 5  * @date 2014-12-8 下午6:54:59  6  * @version V1.0   7  */ 8  9 /**10  * @ClassName: DrawThread211  * @Description: 取钱类12  * @author 任伟13  * @date 2014-12-8 下午6:54:5914  */15 public class DrawThread2 extends Thread {16     private Account2 account; // 用户帐户17     private double drawAmount; // 希望取的钱数18 19     public DrawThread2(String name, Account2 account, double drawAmount) {20         super(name);21         this.account = account;22         this.drawAmount = drawAmount;23     }24 25     /*26      * (non-Javadoc)27      * 28      * @see java.lang.Thread#run()29      */30     @Override31     public void run() {32         account.draw(drawAmount);33     }34 35     public static void main(String[] args) {36         // 创建一个账户37         Account2 acct = new Account2("1234567", 1000);38         // 模拟两个线程对同一个账户取钱39         new DrawThread2("甲", acct, 800).start();40         new DrawThread2("乙", acct, 800).start();41     }42 }
DrawThread2

运行结果:

 

4、延伸
a、synchronized关键字。可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。

b、Domain Driven Design:在面向对象里有一种设计方法:Domain Driven Design(领域驱动设计,DDD),这种方式认为每个类都应该是完备的领域对象,例如Account代表用户帐户,应该提供用户帐户的相关方法;通过draw()方法来执行取钱操作,而不是将setBalance()方法暴露出来任人操作,才能更好地保证Account对象的完整性和一致性。

c、线程安全类特征:通过同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征:
.该类的对象可以被多个线程安全地访问;
.每个线程调用该类的任意方法后都能得到正确的结果;
.每个线程调用该类的任意方法后,该对象状态依然保持合理的状态;

d、可变类和不可变类(Mutable and Immutable Objects):
可变类:当你获得这个类的一个实例引用时,你可以改变这个实例的内容。
不可变类:当你获得这个类的一个实例引用时,你不可以改变这个实例的内容。不可变类的实例一但创建,其内在成员变量的值就不能被修改。

e、如何创建一个自己的不可变类:
.所有成员都是private
.不提供对成员的改变方法,例如:setXXXX
.确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
.如果某一个类成员不是原始变量(primitive)或者不可变类,必须通过在成员初始化(in)或者get方法(out)时通过深度clone方法,来确保类的不可变。

f、不可变类总是线程安全的,因为对象状态不可变;可变类需要额外的方法保证其线程安全。

5、线程安全与运行效率
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,可采用以下策略:
a、不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法同步;
b、如果可变类有两种运行环境:单线程环境和多线程环境,这应该提供该类的线程不安全版本和线程安全版本;

 

多线程总结四:线程同步(一)