多线程之争用条件

标签: 多线程  争用条件  Object  互斥与同步  synchronized锁

什么是争用条件?

当多个线程同时访问同一数据(内存区域)时,每个线程都尝试操作改数据,从而导致数据被破坏(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常用方法:

版权声明:本文为BushQiang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/BushQiang/article/details/80049100