读者朋友,下午好!
今天分享一个很好地讲解并发中竞争条件的例子——银行在多个线程时候,随机在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、加锁怎么加?为什么用公平锁?
待后续补充
好了,再会!