多线程总结四:线程同步(一)
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: Account 11 * @Description: 账户类 12 * @author 任伟 13 * @date 2014-12-8 下午5:35:27 14 */ 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 }
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: DrawThread 11 * @Description: 取钱类 12 * @author 任伟 13 * @date 2014-12-8 下午5:41:46 14 */ 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 @Override 30 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 block 37 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 }
运行结果:
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: SynchronizedBlockDrawThread 11 * @Description: 使用Account作为同步监视器 12 * @author 任伟 13 * @date 2014-12-8 下午5:59:06 14 */ 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 @Override 33 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 block 41 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 }
运行结果:
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 block 50 e.printStackTrace(); 51 } 52 balance -= drawAmount; 53 System.out.println("余额为:" + balance); 54 } else { 55 System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!"); 56 } 57 } 58 }
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: DrawThread2 11 * @Description: 取钱类 12 * @author 任伟 13 * @date 2014-12-8 下午6:54:59 14 */ 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 @Override 31 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 }
运行结果:
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、如果可变类有两种运行环境:单线程环境和多线程环境,这应该提供该类的线程不安全版本和线程安全版本;