admin 管理员组

文章数量: 887021

并发

读者朋友,下午好!

今天分享一个很好地讲解并发中竞争条件的例子——银行在多个线程时候,随机在2个账户之间随机的转金额,在未加锁的时候,账户总金额会出乎意料的不一致;我们希望的是无论怎么转账,银行所有账户的总金额是固定不变的。

示例代码来源

《Java核心技术 卷1 第10版》 Core Java Volume I-Fundamentals(10th Edition)
[美] Cay S.Horstmann 著
周立新 陈波 叶乃文 邝劲筠 杜永萍 译

代码库:

git@github.com:cmhhcm/guiAndConcurrent.git

一、银行转账示例

Bank

package com.cmh.concurrent.unsynch;import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** Author: 起舞的日子* Date:2021/4/18 3:08 下午*/
public class Bank {private final double[] accounts;private Lock bankLock = new ReentrantLock();/*** 初始化银行** @param n              the number of accounts* @param initialBalance the initial balance of each account*/public Bank(int n, double initialBalance) {accounts = new double[n];Arrays.fill(accounts, initialBalance);}/*** 从一个账户给另一个账户转账** @param from* @param to* @param amount*/public void transfer(int from, int to, double amount) {bankLock.lock();try {if (accounts[from] < amount) {return;}System.out.print(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());System.out.println();} finally {bankLock.unlock();}}/*** 来看一下transfer这个方法字节码指令执行情况* javac Bank.java* javap -c -v Bank* <p>* 之后看到的是这样的:* 大体找到对应accounts[from] -= amount的指令:* 22: getfield      #7                  // Field accounts:[D 去from索引位置获取到这个值* 25: iload_1     将第二个int类型的值推送至栈顶* 26: dup2        复制栈顶的数值并将复制值压入栈顶* 27: daload      将double数组指定索引的值推送至栈顶* 28: dload_3     将第四个double型本地变量推送至栈顶* 29: dsub        将栈顶两double型数值相减并将结果压入栈顶* 30: dastore     将栈顶double型数值存入指定数组指定索引的位置* <p>* 通过以上指令,基本知道在accounts[from] = accounts[from] - amount的时候,* 至少需要压栈、详减、存入几个指令,那么在这个过程中,未做并发处理,就会有并发问题。*/public double getTotalBalance() {double sum = Arrays.stream(accounts).sum();return sum;}public int size() {return accounts.length;}
}

BankTest

package com.cmh.concurrent.unsynch;/*** This program shows data corruption when multiple threads access a data structure* <p>* Author: 起舞的日子* Date:2021/4/18 3:08 下午*/
public class UnsynchBankTest {public static final int NACCOUNTS = 100;public static final double INITIAL_BALANCE = 1000;public static final double MAX_ACCOUNT = 1000;public static final int DELAY = 10;public static void main(String[] args) {Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);for (int i = 0; i < NACCOUNTS; i++) {int fromAccount = i;Runnable runnable = () -> {try {while (true) {int toAccount = (int) (bank.size() * Math.random());double amount = MAX_ACCOUNT * Math.random();bank.transfer(fromAccount, toAccount, amount);Thread.sleep((int) (DELAY * Math.random()));}} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}};Thread thread = new Thread(runnable);thread.start();}}}

二、加锁核心代码

未加锁前运行效果:

加锁后运行效果:

三、原理分析

1、为什么会出现总金额不一致的情况?

因为accounts[from] = accounts[from] - amount的时候,
背后的JVM指令不是一个原子性操作,即是一行代码,背后是分几步来完成的。那么在这几步的过程中,就可能被别的线程“抢占”了(操作系统的分配规则)。

通过javap 可以查看编译后的Bank.class文件的这行代码的执行步骤:Bank类中已做详细注释说明。这里在重点讲一下查看流程:

 第一步,编译Bank.java javac Bank.java 第二步:javap -c -v Bank 
即可查看详细指令。-c -v详细含义见下图-

好了,看一下核心执行逻辑:

2、加锁怎么加?为什么用公平锁?

待后续补充

好了,再会!

本文标签: 并发