线程安全问题
什么是线程安全问题
多个线程同时访问同一个资源(变量、对象、文件等)时就可能出现线程安全问题。
多个线程执行时是抢占式的,一个线程在执行一个操作时(调用方法,更新变量),可能会被其他线程打断,导致操作没有完全完成,可能会造成数据出现不一致的情况。
线程安全问题案例:银行转账
/**
* 银行类
* @author xray
*
*/
public class Bank {
//模拟100个账户的余额
int[] accounts = new int[100];
//初始化每个余额为1000
{
for(int i = 0;i < accounts.length;i++){
accounts[i] = 1000;
}
}
/**
* 统计总余额
* @return
*/
public int getTotalAccounts(){
int sum = 0;
for(int i = 0;i < accounts.length;i++){
sum += accounts[i];
}
return sum;
}
/**
* 转账
* @param from 转出账户
* @param to 转入账户
* @param money 金额
*/
public void transfer(int from,int to,int money){
//转出金额
accounts[from] -= money;
System.out.printf("从%d转出%d到%d%n",from,money,to);
//转入金额
accounts[to] += money;
//输出总金额
System.out.println("银行的总金额是:"+getTotalAccounts());
}
}
public class TestBank {
public static void main(String[] args) {
Random random = new Random();
Bank bank = new Bank();
//模拟100次转账
for(int i = 0;i < 100;i++){
//每次转账用新线程执行
new Thread(()->{
//产生转出和转入账户的下标
int from = random.nextInt(bank.accounts.length);
int to = random.nextInt(bank.accounts.length);
//随机金额
int money = 100+random.nextInt(500);
//转账
bank.transfer(from, to, money);
}).start();
}
}
}
执行效果:
账户88向账户90转了142,银行现在的总金额是:98322
账户21向账户27转了61,银行现在的总金额是:98383
账户88向账户23转了150,银行现在的总金额是:98533
账户33向账户45转了128,银行现在的总金额是:97910
账户95向账户34转了92,银行现在的总金额是:98002
....
原因分析
多个线程同时执行transfer方法,一个线程执行转出金额代码,还没执行转入金额,就被其他线程抢占执行了,这样其它线程调用getTotalAccounts方法进行统计时金额就少了。
线程同步问题的解决方法
线程同步问题可以通过上锁机制解决,主要是将资源上锁,让线程将业务完整的完成,不让其他线程介入。
三种锁机制
1、同步代码块
synchronized(锁对象){
需要同步执行的代码
}
注意:任何对象都可以作为锁,必须是成员变量
原理:一旦线程进入了该代码块,就持有锁,JVM会有监视器监视进入锁的线程,其它线程想进入代码,监视器会拒绝访问;一旦持有锁的线程执行代码完毕,锁就被释放,其它线程就可以进入。
使用同步块修改transfer方法:
public void transfer(int from,int to,int money){
synchronized(this){
//转出金额
accounts[from] -= money;
System.out.printf("从%d转出%d到%d%n",from,money,to);
//转入金额
accounts[to] += money;
//输出总金额
System.out.println("银行的总金额是:"+getTotalAccount());
}
}
执行效果:
从93转出198到31 银行的总金额是:100000
从5转出164到57 银行的总金额是:100000
从83转出62到57 银行的总金额是:100000
从22转出58到3 银行的总金额是:100000
2、同步方法
public synchronized 返回值 方法名(参数..){
方法的代码
}
同步块和同步方法的区别:
- 同步块可以控制代码的范围,同步方法是整个方法上锁
- 同步块可以将任意的成员变量作为锁,同步方法只能以this作为锁
- 同步块的性能高于同步方法
使用同步方法修改transfer方法:
public synchronized void transfer(int from,int to,int money){
//转出金额
accounts[from] -= money;
System.out.printf("从%d转出%d到%d%n",from,money,to);
//转入金额
accounts[to] += money;
//输出总金额
System.out.println("银行的总金额是:"+getTotalAccount());
}
3、同步锁
Java1.5后出现的Lock包括:
- ReentrantLock 重入锁,控制线程进入
- ReadLock 读锁,控制线程读取
- WriteLock 写锁,控制线程写入
- ReadWriteLock 读写锁,控制线程读写
用法:
锁对象.lock();
try{
需要同步的代码
}finally{
锁对象.unlock();
}
注意:锁对象不能是局部变量
使用同步锁修改transfer方法:
//同步锁对象
private ReentrantLock lock = new ReentrantLock();
public void transfer(int from,int to,int money){
//同步锁上锁
lock.lock();
try{
//转出金额
accounts[from] -= money;
System.out.printf("从%d转出%d到%d%n",from,money,to);
//转入金额
accounts[to] += money;
//输出总金额
System.out.println("银行的总金额是:"+getTotalAccount());
}finally{
//释放锁
lock.unlock();
}
}
三种上锁机制的总结:
- 同步块和同步方法出现早,同步锁在1.5后出现
- 性能:同步锁 > 同步块 > 同步方法
- 同步锁提供了大量的方法,也可以if配合使用,更加灵活
大家如果需要学习其他Java知识点,戳这里 超详细的Java知识点汇总