java多线程三:线程同步
?? 多线程编程是很有趣的事情,它常常容易出现"错误情况",这是由于系统的线程调度具有一定的随机性。当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。
例如:一个经典的例子,银行取钱的问题。
package com.yt.manager.thread.synchronus;/** * @Description:帐户信息 * @ClassName: Account * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class Account {public Account(String accountNo, double balance) {this.accountNo = accountNo;this.balance = balance;}private String accountNo;private double balance;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;}}
?假设两个线程同时做取钱的操作:
package com.yt.manager.thread.synchronus;/** * @Description: 此例演示多线程环境下出现的非同步问题 * @ClassName: DrawThread * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */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);account.setBalance(account.getBalance() - drawAmount);System.out.println("余额为:" + account.getBalance());} else {System.out.println("帐户余额不足!");}}public static void main(String[] args) {// 创建一个帐户Account account = new Account("123123", 1000);// 模拟两条线程对同一个帐户取钱new DrawThread("甲", account, 800).start();new DrawThread("乙", account, 800).start();}}
?您会看到如下的结果:
取钱成功:800.0余额为:200.0取钱成功:800.0余额为:-600.0
?1、为了解决这个问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码快:
synchronized(obj){//此处为同步代码块的内容}
?虽然Java允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止多条线程对同一个资源的并发访问。因此通常推荐把可能被并发方法的共享资源作为同步监视器。对于上面的程序,我们应该考虑使用帐号(account)作为同步监视器。
package com.yt.manager.thread.synchronus;/** * @Description: 使用synchronized同步代码块来解决多线程下数据安全问题 * @ClassName: DrawThread * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class DrawThreadSynchronized extends Thread {private Account account;private double drawAmount;public DrawThreadSynchronized(String name, Account account,double drawAmount) {super(name);this.account = account;this.drawAmount = drawAmount;}public void run() {// 线程开始执行同步代码块之前,必须先获得对同步监视器对象(account)的锁定// 加锁--修改完成--释放锁synchronized (account) {if (account.getBalance() >= drawAmount) {System.out.println("取钱成功:" + drawAmount);account.setBalance(account.getBalance() - drawAmount);System.out.println("余额为:" + account.getBalance());} else {System.out.println("帐户余额不足!");}}}public static void main(String[] args) {// 创建一个帐户Account account = new Account("123123", 1000);// 模拟两条线程对同一个帐户取钱new DrawThreadSynchronized("甲", account, 800).start();new DrawThreadSynchronized("乙", account, 800).start();}}
?2、使用同步方法来解决数据安全问题
package com.yt.manager.thread.synchronus;/** * @Description:用户帐号类 * @ClassName: Account * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class Account {public Account(String accountNo, double balance) {this.accountNo = accountNo;this.balance = balance;}private String accountNo;private double balance;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;}/** * 同步方法:测试的draw方法直接写在Account类里面,而不是在run方法中时间取钱逻辑,更符合面向对象的规则。 * * @param drawAmount * 要取的金额 */public synchronized void draw(double drawAmount) {if (this.balance >= drawAmount) {System.out.println("取钱成功:" + drawAmount);this.balance = this.balance - drawAmount;System.out.println("余额为:" + this.balance);} else {System.out.println("帐户余额不足!");}}}
package com.yt.manager.thread.synchronus;/** * @Description: 使用同步方法来解决数据安全问题 * @ClassName: DrawThread * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class DrawThreadSynchronizedMethod extends Thread {private Account account;private double drawAmount;public DrawThreadSynchronizedMethod(String name, Account account,double drawAmount) {super(name);this.account = account;this.drawAmount = drawAmount;}// 当多线程修改同一条共享数据时,将会出现数据安全问题public void run() {// 直接调用account的同步draw方法来取钱account.draw(drawAmount);}public static void main(String[] args) {// 创建一个帐户Account account = new Account("123123", 1000);// 模拟两条线程对同一个帐户取钱new DrawThreadSynchronizedMethod("甲", account, 800).start();new DrawThreadSynchronizedMethod("乙", account, 800).start();}}
?在面向对象中,有一种流行的设计方法:Domain Driven Design(领域驱动设计,简称DDD),这种方式认为每个类应该都是完备的领域对象。如:Account代表用户帐户类,它应该提供相关的用户帐户方法,例如通过draw方法来执行取钱操作,而不是让setBalance()方法暴露在任何人面前, 这样才能尽可能的保证Accoung类的完整性和一致性。
3、使用同步锁(Lock)
?Java提供了另外一种线程同步机制:它通过显式的定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象来充当。
package com.yt.manager.thread.synchronus;import java.util.concurrent.locks.ReentrantLock;/** * @Description:用户帐户类 * @ClassName: Account * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class AccountLock {private final ReentrantLock lock = new ReentrantLock();public AccountLock(String accountNo, double balance) {this.accountNo = accountNo;this.balance = balance;}private String accountNo;private double balance;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;}/** * 使用同步锁来进行局部同步 * * @param drawAmount * 要取的金额 */public void draw(double drawAmount) {lock.lock();try {if (this.balance >= drawAmount) {System.out.println("取钱成功:" + drawAmount);this.balance = this.balance - drawAmount;System.out.println("余额为:" + this.balance);} else {System.out.println("帐户余额不足!");}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}}
package com.yt.manager.thread.synchronus;/** * @Description: 使用同步锁来解决数据安全问题 * @ClassName: DrawThread * @Project: base-info * @Author: zxf * @Date: 2011-7-20 */public class ThreadSynchronizedLock extends Thread {private AccountLock account;private double drawAmount;public ThreadSynchronizedLock(String name, AccountLock account,double drawAmount) {super(name);this.account = account;this.drawAmount = drawAmount;}// 当多线程修改同一条共享数据时,将会出现数据安全问题public void run() {// 直接调用account的同步draw方法来取钱account.draw(drawAmount);}public static void main(String[] args) {// 创建一个帐户AccountLock account = new AccountLock("123123", 1000);// 模拟两条线程对同一个帐户取钱new ThreadSynchronizedLock("甲", account, 800).start();new ThreadSynchronizedLock("乙", account, 800).start();}}?
?
?
?