聊聊多线程(三)线程安全问题

超详细的Java知识点路线图


线程安全问题

什么是线程安全问题

多个线程同时访问同一个资源(变量、对象、文件等)时就可能出现线程安全问题。

多个线程执行时是抢占式的,一个线程在执行一个操作时(调用方法,更新变量),可能会被其他线程打断,导致操作没有完全完成,可能会造成数据出现不一致的情况。

线程安全问题案例:银行转账
/**
 * 银行类
 * @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 返回值 方法名(参数..){
    方法的代码
}

同步块和同步方法的区别:

  1. 同步块可以控制代码的范围,同步方法是整个方法上锁
  2. 同步块可以将任意的成员变量作为锁,同步方法只能以this作为锁
  3. 同步块的性能高于同步方法

使用同步方法修改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. 同步块和同步方法出现早,同步锁在1.5后出现
  2. 性能:同步锁 > 同步块 > 同步方法
  3. 同步锁提供了大量的方法,也可以if配合使用,更加灵活

大家如果需要学习其他Java知识点,戳这里 超详细的Java知识点汇总

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恒哥~Bingo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值