多线程编程是很有趣的事情,它常常容易出现"错误情况",这是由于系统的线程调度具有一定的随机性。当使用多个线程来访问同一个数据时,非常容易出现线程安全问题。
例如:一个经典的例子,银行取钱的问题。
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();
}
}