线程安全问题:多个线程并行访问共享资源所引发的业务安全问题。线程安全问题可通过线程同步来解决。
以 “银行取款” 为例,假设用户A、用户B共享账户( id=001,balance=1000),用户A、B同时执行取款1000元:
public class Account {
private String id; //卡号
private double balance; //余额
public Account(String id, double balance) {
this.id = id;
this.balance = balance;
}
/**
* 模拟取款操作
* @param money 取款金额
*/
public void withdraw(double money){
//1、判断余额是否充足
if(balance<money){
//1.1 余额不足,取款失败
return ;
}
//2、余额充足,取款成功
balance-=money;
}
}
- 用户A取款,判断账户余额是否充足(账户余额1000,允许取款)
- 用户B取款,判断账户余额是否充足(账户余额1000,允许取款)
- 用户A取款1000,账户余额0
- 用户B取款1000,账户余额-1000,引发线程安全问题。
解决方案一:同步代码块
为访问共享资源的代码块上锁。同一时刻只允许一个线程成功获得锁,只有成功获取锁的线程才可访问共享资源。
synchronized (锁对象){
//访问共享资源的代码块...
}
/*
建议使用共享资源作为锁对象。同一时刻只有一个线程可成功获取锁,使用共享资源作为锁对象可保证同一时刻只有一个线程可获取共享资源。
--实例方法使用this关键字表示共享资源
--静态方法使用字节码(类名.class)表示共享资源
*/
以上述 “”银行账户取款引发的线程安全问题” 为例:为取款代码块上锁
/**
* 模拟取款操作
* @param money 取款金额
*/
public void withdraw(double money){
synchronized (this){
//1、判断余额是否充足
if(balance<money){
//1.1 余额不足,取款失败
return ;
}
//2、余额充足,取款成功
balance-=money;
}
}
- 用户A取款,尝试获取锁,获取成功
- 用户B取款,尝试获取锁,获取失败,等待...
- 用户A取款,判断账户余额是否充足(账户余额充足,允许取款)
- 用户B取款,尝试获取锁,获取失败,等待...
- 用户A取款1000,账户0,释放锁
- 用户B取款,尝试获取锁,获取成功
- 用户B 取款,判断账户余额是否充足(账户余额不足,拒绝取款)
解决方案二:同步方法
为访问共享资源的方法上锁。
修饰符 synchronized 返回值类型 方法名(形参列表){
//访问共享资源...
}
同步方法与同步代码块区别在于上锁的范围不同,上锁的范围越小,性能越高。
解决方案三:Lock锁
Lock锁是Java提供的、允许程序员自行创建的锁。Lock是接口,不允许直接实例化,可通过实现类ReentrantLock创建锁对象。
public class Account {
private String id; //卡号
private double balance; //余额
//创建一个锁对象,final关键字修饰表示该属性不可改
private final Lock myLock=new ReentrantLock();
public Account(String id, double balance) {
this.id = id;
this.balance = balance;
}
/**
* 模拟取款操作
* @param money 取款金额
*/
public void withdraw(double money){
//1、上锁
myLock.lock();
try{
//余额不足,取款失败
if(balance<money){
return ;
}
//余额充足,取款成功
balance-=money;
}catch (Exception e){
e.printStackTrace();
}finally {
//2、释放锁
myLock.unlock();
}
}
}
注意事项:建议使用try...catch...finally语句访问共享资源。无论程序执行是否出现异常,最终都会执行unlock( )释放锁,避免出现死锁。