JAVA进阶学习-多线程基础详解(二)-同步

  之前有些朋友读了第一篇博客之后说我写的有点晦涩,不够通俗易懂,想了一下言之有理,决定在以后的博客里换个风格,总之大家一起共勉吧,我也不是什么大神,有啥问题大家一定要提出来,本人有则改之,无则加勉!


  之前的一篇博客里给大家介绍和分析了创建线程的两种方法,这篇博客我们就来分析下关于线程同步的一些问题。

  在实际开发中我们经常见到这样的一种情况:两个或两个以上的线程存取同一个对象,并且每个线程都调用了一个修改该对象的方法,这时错误就会在你不经意间发生了,这是为什么呢?我们来举一个例子:现在有一升水,小明负责给这一升水里面加水,每次半升;小强则负责从这一升水里面舀水,每次也是半升,且同一时刻仅有一个人可以进行操作。如果两人你加水、我舀水这样持续下去的化,按理说这一升水的总量应该是不变的,但是如果小明加水时半升水还没有加完,小强就过来舀水,这时错误就出现了(例子举得不好,意思差不多是个这意思)。

  为了避免多线程引起的对共享数据的讹误,我们必须了解如何进行同步存取。这篇博客,我先给大家看看如果没有使用同步会怎么样。下篇博客,再来分析如何同步存取数据(也就是“锁”)。

  下面的这个例子是《Java核心技术》中的一个例子,这个例子很好的展现了没有使用同步时的情况,之后讲解同步的时候也将在这个例子的基础上进行修改。

  我们模拟一个有着若干账户的银行。随机的生成在这些账户之间转移钱款的交易。每一个账户有一个线程。每一笔交易中,会从线程所服务的账户中随即转移一定数目的钱款到另一个随机账户。当这个程序运行的时候,我们不清楚在某一时刻某一账户中有多少钱。但是,知道所有账户的总金额应该保持不变,因为我们所做的只不过是从一个账户转移钱款到另一个账户。以下是该程序的代码。

首先是Bank类,该类有一个transfer方法,负责从一个账户转移一定数目的钱款到另一个账户。

package demo2.unsynch;


public class Bank {

	private final double[] accounts; // 账户数组

	public Bank(int n, double initialBalance) { // 构造方法 初始化账户余额

		accounts = new double[n];
		for (int i = 0; i < accounts.length; i++) {
			accounts[i] = initialBalance;
		}

	}

	/**
	 * 从一个账户转移一定的钱款到另一个账户
	 *
	 * @param from    原账户
	 * @param to   目标账户
	 * @param amount  钱数
	 */
	public void transfer(int from, int to, double amount) {

		if (accounts[from] < amount)
			return;
		System.out.println(Thread.currentThread());
		accounts[from] -= amount; // 从原账户中转出钱款
		System.out.printf(" %10.2f from %d to %d", amount, from, to);
		accounts[to] += amount; // 把转出的钱款转移到目标账户
		System.out.printf("Total Balance: %10.2f%n", getTotalBalance());

	}

	/**
	 * 统计银行总存款
	 *
	 * @return 总存款
	 */
	public double getTotalBalance() {
		double sum = 0;
		for (double a : accounts) {
			sum += a;
		}
		return sum;
	}

	public int size() {
		return accounts.length;
	}

}

TransferRunnable类实现了Runnable接口,它的run方法不断地从一个固定的银行账户取出钱款。在每一次迭代中,run方法随机选择一个目标账户和一个随机账户,调用bank对象的transfer方法,然后睡眠。

package demo2.unsynch;

public class TransferRunnable implements Runnable {

	private Bank bank;
	private int fromAccount;
	private double maxAmount;
	private int DELAY = 10;

	public TransferRunnable(Bank b, int from, double max) {

		bank = b;
		fromAccount = from;
		maxAmount = max;

	}

	public void run() {

		try {
			while (true) {
				int toAccount = (int) (bank.size() * Math.random());
				double amount = maxAmount * Math.random();
				bank.transfer(fromAccount, toAccount, amount);
				Thread.sleep((int) (DELAY * Math.random()));
			}
		} catch (Exception e) {
			// TODO: handle exception
		}

	}

}

之后是测试类UnsynchBankTest

package demo2.unsynch;

public class UnsynchBankTest {

	public static final int NACCOUNTS = 100; //账户数量
	public static final int INITIAL_BALANCE = 1000; //初始余额

	public static void main(String[] args){

		Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
		int i;
		for(i=0;i<NACCOUNTS;i++){
			TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
			Thread t = new Thread(r);
			t.start();
		}

	}

}

  运行UnsynchBankTest我们来观察一下结果:

     750.45 from 3 to 89Total Balance:  100000.00
Thread[Thread-19,5,main]
     106.04 from 19 to 55Total Balance:  100000.00
Thread[Thread-65,5,main]
     250.99 from 65 to 87Total Balance:  100000.00
Thread[Thread-57,5,main]
     735.54 from 57 to 98Total Balance:  100000.00
Thread[Thread-17,5,main]
     770.54 from 17 to 95Total Balance:  100000.00
Thread[Thread-27,5,main]
     723.10 from 27 to 53Total Balance:  100000.00
Thread[Thread-44,5,main]
     265.50 from 44 to 76Total Balance:  100000.00
Thread[Thread-65,5,main]
     547.80 from 65 to 83Total Balance:  100000.00
Thread[Thread-11,5,main]
Thread[Thread-16,5,main]
     488.78 from 16 to 49Total Balance:   99889.86
Thread[Thread-74,5,main]
      40.39 from 74 to 26Total Balance:   99889.86
Thread[Thread-66,5,main]
     492.19 from 66 to 68Total Balance:   99889.86
Thread[Thread-4,5,main]
      10.67 from 4 to 38Total Balance:   99889.86
Thread[Thread-94,5,main]

  以上是一组典型的输出,我们很清楚的可以看到,错误还是出现了,在最初几次的交易中,银行的余额还是保持在100 000,这时正确的,因为共有100个账户,每个账户1 000。但是,过一段时间会发现错误发生了。总额要么增加,要么减少。当两个线程试图更新同一个账户的时候,这个问题就出现了。

  其实真整的问题在于transfer方法的执行过程中可能会被中断。如果我们能够确保线程在失去控制之前方法一定会运行完成,那么这个银行账户对象就永远不会出现讹误。这些内容因为要写很长的篇幅,我在下一篇博客中给大家呈现。


笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
版权声明:本文为博主原创文章,未经博主允许不得转载。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦