什么是争用条件?
当多个线程同时访问同一数据(内存区域)时,每个线程都尝试操作改数据,从而导致数据被破坏(corrupted),这种现象称为争用条件。
下面用一个案例来说明。
案例:100个人之间进行相互转账,每个人都有一张银行卡,卡里1000元。在转账的时候都由银行的系统来操作。
银行系统:
package com.smxy.people; /** * * @Description 银行转账系统,所有的人都在这里进行转账业务 * @author Bush罗 * @date 2018年4月23日 * */ public class MoneySystem { //每个人装钱的,类似于银行卡 private int peopleMoney[]; /** * * @param num 有多少张卡 * @param Money 每张卡多少钱 */ public MoneySystem(int num,int Money) { super(); peopleMoney=new int[num]; for(int i=0;i<num;i++){ System.out.println(i); peopleMoney[i]=Money; } } /** * * @param form 谁转的钱 * @param to 转给谁 * @param money 转的钱 */ public void transfer(int from,int to,int money){ //如果自己的钱小于要转的钱就返回 if(peopleMoney[from]<money){ return; } peopleMoney[from]-=money; peopleMoney[to]+=money; System.out.print("people"+from+"转账给people"+to+""+money); System.out.println("所有的钱总和为"+allMoney()); } /** * 计算所有人的钱 * @return */ public int allMoney(){ int sum=0; for(int i=0;i<peopleMoney.length;i++){ sum+=peopleMoney[i]; } return sum; } }
线程转账:
package com.smxy.people; /** * * @Description 线程转账 * @author Bush罗 * @date 2018年4月23日 * */ public class MoneyTransferTask implements Runnable{ //转账系统 private MoneySystem moneySystem; //转账的人 private int fromPeople; //单次转账的最大金额 private int maxMoney; //最大休眠时间(毫秒) private int delay = 10; public MoneyTransferTask(MoneySystem moneySystem, int fromPeople, int maxMoney) { super(); this.moneySystem = moneySystem; this.fromPeople = fromPeople; this.maxMoney = maxMoney; } @Override public void run() { // TODO Auto-generated method stub while(true){ int toPeople = (int) (99*Math.random());//转给谁 int money = (int)(maxMoney * Math.random()); moneySystem.transfer(fromPeople, toPeople, money); try { Thread.sleep((int) (delay * Math.random())); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
测试代码:
package com.smxy.people; /** * * @Description 测试用例 * @author Bush罗 * @date 2018年4月23日 * */ public class SystemTest { //总共有100个人在互相转账,每个人都是独立的线程 private final static int Peoplesum=100; //每个人开始的总金额 private final static int Money=1000; public static void main(String[] args) { //银行系统初始化 MoneySystem moneySystem=new MoneySystem(Peoplesum,Money); //开启100个线程,进行互相转账 for(int i=0;i<100;i++){ MoneyTransferTask task=new MoneyTransferTask(moneySystem,i,Money); Thread thread=new Thread(task,"TransferThread_"+i); thread.start(); } } }
测试结果:
我们发现钱的总和变少了,那么为什么会产生这样的结果呢?
我们知道在同一时间只能有一条线程在cup上运行,而线程之间的调度是通过分时和抢占来完成的。
假设银行有1000块钱,如果在某一时间线程1获得cup的资源,获取银行目标数值1000,然后将转移的钱200添加,使得总的钱为1200,但是这是钱并没有写回银行系统,而是留在线程自己的内存之中,此时线程1的操作时间耗尽,线程调度切换线程2获得cup资源,线程2同样获取银行目标数值1000,同时将添加500,变为1500,然后将修改后的值写入银行系统。这时银行目标数值为1500,此时线程1又获得了cup资源继续执行它的上下文,把1200写入银行系统,这是银行的钱就变成1200了。两个线程由于访问了共享的数据区域,通过写的操作破坏了共享数据的值,使得我们产生了争用条件的情况。总共是添加了两次分别为200和500,但是最后只添加了500.损失了200.这就是为什么钱会少了的原因。
那么我们应该如何来避免这样的情况呢
通过互斥和同步就可以实现我们线程之间正确的交互,达到线程之间正确处理数据的要求。
互斥同步实现:通过增加一个锁来实现 synchronized+Object对象
银行改进代码:
private final Object lockObj = new Object(); /** * * @param form * 谁转的钱 * @param to * 转给谁 * @param money * 转的钱 */ public void transfer(int from, int to, int money) { synchronized (lockObj) { // 如果自己的钱小于要转的钱就返回 /*if (peopleMoney[from] < money) { return; }*/ //while循环,保证条件不满足时任务都会被条件阻挡 //而不是继续竞争CPU资源 while (peopleMoney[from] < money){ try { //条件不满足, 将当前线程放入Wait Set lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } peopleMoney[from] -= money; peopleMoney[to] += money; System.out.print(Thread.currentThread().getName() + " "); System.out.print(" people" + from + "转账给people" + to + " " + money); System.out.println(" 所有的钱总和为" + allMoney()); //唤醒所有在lockObj对象上等待的线程 lockObj.notifyAll(); } }
原来条件不满足是直接返回,但是线程返回后任然有机会获取cup资源从而再次要求加锁,而加锁是有开销的,会降低系统的性能。应该用while做判断,如果不满足条件就lockObj.wait();是线程进入一个等待的状态。任务结束后使用lockObj.notifyAll(); 告诉那些在等待的线程我已经执行完了,你们可以来做,然后他们又开始各凭本事争夺这个机会。如果是notify()方法就会唤醒在此对象监视器上等待的单个线程。
效果图:总金额不变
Thread常用方法: