并发-2-竞争条件例子、锁对象

3 篇文章 0 订阅
本文通过一个银行转账的并发示例,展示了在无锁情况下,多线程访问导致的账户总金额不一致问题。通过分析字节码指令,揭示了非原子操作在并发环境中的风险。并介绍了使用ReentrantLock加锁后的解决方案,确保转账操作的正确性。同时,提供了查看字节码指令的方法,帮助理解并发编程中的细节。
摘要由CSDN通过智能技术生成

读者朋友,下午好!

今天分享一个很好地讲解并发中竞争条件的例子——银行在多个线程时候,随机在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、加锁怎么加?为什么用公平锁?

待后续补充

好了,再会!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值