实际开发中,有很多场景都会出现多个线程共享相同内存空间(变量)的情况,由于多个线程并发操作相同的内存空间,此时就极有可能出现数据不安全的问题,比如经典的银行取钱问题:
有一个银行账户,还有余额1100元,现在A通过银行卡从中取1000元,而同时另外一个人B通过存折也从这个账户中取1000元。取钱之前,要首先进行判断:如果账户中的余额大于要取的金额,则可以执行取款操作,否则,将拒绝取款。
我们假定有两个线程来分别从银行卡和存折进行取款操作,当A线程执行完判断语句后,获得了当前账户中的余额数(1000元),因为余额大于取款金额,所以准备执行取钱操作(从账户中减去1000元),但此时它被线程B打断,然后,线程B根据余额,从中取出1000元,然后,将账户里面的余额减去1000元,然后,返回执行线程A的动作,这个线程将从上次中断的地方开始执行:也就是说,它将不再判断账户中的余额,而是直接将上次中断之前获得的余额减去1000。此时,经过两次的取款操作,账户中的余额为100元,从账面上来看,银行支出了1000元,但实际上,银行支出了2000元。
Account.java
/**
* 账号类
* @author mrchai
*
*/
public class Account {
private int id;
private double money;
public Account() {
// TODO Auto-generated constructor stub
}
public Account(int id, double money) {
super();
this.id = id;
this.money = money;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
AccountManager类
public class AccountManager {
//需要被操作的账户对象
private Account account;
public AccountManager(Account account) {
super();
this.account = account;
}
public void getMoney(double cash){
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}
}
public class PeopleThread extends Thread{
private AccountManager manager;
private double cash;
public PeopleThread(AccountManager manager,double cash) {
super();
this.manager = manager;
this.cash = cash;
}
@Override
public void run() {
manager.getMoney(cash);
}
public static void main(String[] args) {
//初始化一个账户(确保只有一个账户)
Account a = new Account(1,10000);
AccountManager manager = new AccountManager(a);
//创建两个线程对象,传入操作账户的管理对象(唯一)和取款金额
PeopleThread t1 = new PeopleThread(manager,10000);
PeopleThread t2 = new PeopleThread(manager,10000);
t1.start();
t2.start();
}
}
结果
线程进入:Thread-1
线程进入:Thread-0
取款成功,取款:10000.0,余额0.0
取款成功,取款:10000.0,余额0.0
通过观察结果,得知,账户实际只有10000元,但是两个线程都取款成功,这就出现了线程并发数据不一致的严重问题,如何解决问题?
以上银行取款问题只是众多线程并发产生的安全问题中的一种,还有很多情况,比如:秒杀,抢券,抢票;因此在这问题产生后,就需要一种排队机制的引入;在Java中每一个对象都存在一个互斥锁(排他锁,对象锁),具体的使用是通过synchronizad关键字将对象锁定,此时,如果对象被一个线程锁定,则其他线程无法在操作当前对象,只有等待拥有该对象锁的线程释放锁之后,其他线程才能使用该对象。
public void getMoney(double cash){
//将对象锁定,当前线程释放对象锁之前,其他线程无法访问
synchronized (account) {
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}
}
以上操作即称之为线程同步。因此,synchronized语句块也称之同步块,synchronized不仅可以用于同步块,还能适用于同步方法,即在方法的声明上使用该关键字,如下
public synchronized void getMoney(double cash){
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}