线程同步会在偶然的多线程并发访问情况下出现线程安全的情况,例如银行的取钱存钱操作等都是需要保证数据的一致性,和每次操作的线程安全。
现在使用2个线程来模拟银行取款操作,模拟2个人使用同一个账户并发取钱的问题。
注意: 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
一、线程安全问题
现在使用2个线程来模拟银行取款操作,模拟2个人使用同一个账户并发取钱的问题。
- 定义Account类
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; public class Account { private String accoutnNo; // 账号编号 private double balance;// 余额 public Account(String accoutnNo, double balance) { super(); this.accoutnNo = accoutnNo; this.balance = balance; } public String getAccoutnNo() { return accoutnNo; } public void setAccoutnNo(String accoutnNo) { this.accoutnNo = accoutnNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj != null && obj.getClass() == Account.class) { Account a = (Account) obj; return a.getAccoutnNo().equals(accoutnNo); } return false; } @Override public int hashCode() { return accoutnNo.hashCode(); } }
- 定义取钱的线程类 DrawThread
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; /** * 取钱操作 */ public class DrawThread extends Thread { private Account account;// 模拟账户 private double drawMoney; // 取钱金额 public DrawThread(String name, Account account, double drawMoney) { super(name); this.account = account; this.drawMoney = drawMoney; } @Override public void run() { if (account.getBalance() >= drawMoney) { System.out.println(getName() + "取钱成功!取了 " + drawMoney + "元"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.setBalance(account.getBalance() - drawMoney); System.out.println(getName() + ": 账户" + account.getAccoutnNo() + "剩余:" + account.getBalance()); } else { System.out.println(getName() + "取钱失败"); } } } }
- 主测试类
// 创建一个账户,让两个线程来操作 Account account = new Account("0001", 1000); new DrawThread("A", account, 800).start(); new DrawThread("B", account, 800).start();
- 结果
A取钱成功!取了 800.0元 B取钱成功!取了 800.0元 A: 账户0001剩余:200.0 B: 账户0001剩余:-600.0
二、使用同步代码块
- 取款的线程类
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; /** * 取钱操作 */ public class DrawThread extends Thread { private Account account;// 模拟账户 private double drawMoney; // 取钱金额 public DrawThread(String name, Account account, double drawMoney) { super(name); this.account = account; this.drawMoney = drawMoney; } //加锁-》修改锁-》释放锁 //使用可能并发访问的共享资源作为同步监视器 @Override public void run() { synchronized (account) { if (account.getBalance() >= drawMoney) { System.out.println(getName() + "取钱成功!取了 " + drawMoney + "元"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.setBalance(account.getBalance() - drawMoney); System.out.println(getName() + ": 账户" + account.getAccoutnNo() + "剩余:" + account.getBalance()); } else { System.out.println(getName() + "取钱失败"); } } //释放同步锁 } }
注意: 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
三、使用同步方法
- 修改后的Account类
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; public class Account2 { private String accoutnNo; // 账号编号 private double balance;// 余额 public Account2(String accoutnNo, double balance) { super(); this.accoutnNo = accoutnNo; this.balance = balance; } public String getAccoutnNo() { return accoutnNo; } public void setAccoutnNo(String accoutnNo) { this.accoutnNo = accoutnNo; } public double getBalance() { return balance; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj != null && obj.getClass() == Account2.class) { Account2 a = (Account2) obj; return a.getAccoutnNo().equals(accoutnNo); } return false; } @Override public int hashCode() { return accoutnNo.hashCode(); } /** * 添加一个取钱的同步方法 该方法的同步监视器是this,对于同一个账户,任何时刻只有一个线程获得对Account2对象的锁定, * 然后进入draw方法执行取钱操作,这样就可以保证多个线程并发取钱的线程安全。 */ public synchronized void draw(double drawAmount) { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName() + "取钱成功,本次取出" + drawAmount); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } balance -= drawAmount; System.out.println(Thread.currentThread().getName() + "取钱之后账户还剩余:" + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足!"); } } }
- 取钱操作的线程类
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; /** * 取钱操作 */ public class DrawThread2 extends Thread { private Account2 account2;// 模拟账户 private double drawMoney; // 取钱金额 public DrawThread2(String name, Account2 account2, double drawMoney) { super(name); this.account2 = account2; this.drawMoney = drawMoney; } @Override public void run() { account2.draw(drawMoney); } }
- 测试如上示例
- 提示
在Account类里面定义draw方法而不是在run方法中实现取钱逻辑这种做法更加符合面向对象的规则,符合DDD(领域驱动设计)
四、使用同步锁
在实现线程安全的控制中,比较常用的是可重入锁(ReentrantLock),使用该Lock对象可以显示的加锁、释放锁。
- 修改后的Account类
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; import java.util.concurrent.locks.ReentrantLock; public class Account3 { // 该锁具有可重入性,一个线程可以对已被加锁的ReentrantLock再次加锁 private final ReentrantLock lock = new ReentrantLock(); // 定义锁对象 private String accoutnNo; // 账号编号 private double balance;// 余额 public Account3(String accoutnNo, double balance) { this.accoutnNo = accoutnNo; this.balance = balance; } public String getAccoutnNo() { return accoutnNo; } public void setAccoutnNo(String accoutnNo) { this.accoutnNo = accoutnNo; } // 余额不可以随便修改,故而只能提供get方法 public double getBalance() { return balance; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj != null && obj.getClass() == Account3.class) { Account3 a = (Account3) obj; return a.getAccoutnNo().equals(accoutnNo); } return false; } @Override public int hashCode() { return accoutnNo.hashCode(); } public void draw(double drawAmount) { <strong><span style="color:#FF0000;">lock.lock();</span></strong> try { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName() + "取钱成功,本次取出" + drawAmount); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } balance -= drawAmount; System.out.println(Thread.currentThread().getName() + "取钱之后账户还剩余:" + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足!"); } } finally { <strong><span style="color:#FF0000;"> lock.unlock();</span></strong> } } }
- 提示
ReentrantLock可重入锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,该锁对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()方法来释放锁,所以一段被锁的代码可以调用另一个被相同锁保护的方法。
五、死锁
-
程序实例
/* * Creation : 2015年10月15日 */ package com.tan.thread.bank; /** * 当两个线程相互等待对方释放同步监视器时就会发生死锁 */ public class DeadLockTest implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); a.foo(b); System.out.println("进入主线程之后..."); } @Override public void run() { Thread.currentThread().setName("副线程"); b.bar(a); System.out.println("进入副线程之后..."); } public static void main(String[] args) { DeadLockTest lockTest = new DeadLockTest(); new Thread(lockTest).start(); lockTest.init(); } } class A { public synchronized void foo(B b) { System.out.println(Thread.currentThread().getName() + "进入A中的foo方法"); try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "企图调用B示例的last方法..."); b.last(); } public synchronized void last() { System.out.println(Thread.currentThread().getName() + "进入A类的last方法中..."); } } class B { public synchronized void bar(A a) { System.out.println(Thread.currentThread().getName() + "进入B中的bar方法"); try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "企图调用A示例的last方法..."); a.last(); } public synchronized void last() { System.out.println(Thread.currentThread().getName() + "进入B类的last方法"); } }