Java多线程中原子性、可见性、有序性以及竟态条件案例

原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

代码案例

用一个简单的银行账户转账收款的代码例子来看,其中包含了账户实体模型,实现了提款、收款接口

package org.example.transfer;

import java.math.BigDecimal;

/** 账户实体
 * @author : kenny
 * @since : 2024/8/17
 **/
public class AccountEntity implements WithdrawAbility, ReceivingAbility{
    private Long id;
    private BigDecimal balance;
    private final Object accountLock = new Object();

    public AccountEntity(Long id, BigDecimal balance) {
        this.id = id;
        this.balance = balance;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public BigDecimal getBalance() {
        return balance;
    }

    @Override
    public boolean withdraw(BigDecimal withDrawAmount) {
        if (withDrawAmount == null || withDrawAmount.compareTo(BigDecimal.valueOf(0)) <= 0){
            throw new RuntimeException("当前账户" + id +  " " + "提取金额不能为0");
        }

        if (balance == null || balance.compareTo(BigDecimal.valueOf(0)) <= 0){
            throw new RuntimeException("当前账户" + id +  " " + "账户余额不足");
        }

        BigDecimal afterBalance = balance.subtract(withDrawAmount);
        if (afterBalance.compareTo(BigDecimal.valueOf(0)) <= 0){
            throw new RuntimeException("当前账户" + id +  " " + "账户余额不足");
        }

        balance = afterBalance;
        return true;
    }

    @Override
    public boolean receive(BigDecimal amount) {
        if (amount == null || amount.compareTo(BigDecimal.valueOf(0)) <= 0){
            throw new RuntimeException("当前账户" + id +  " " +"收款异常");
        }

        balance = balance.add(amount);
        return true;
    }
}
package org.example.transfer;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author : kenny
 * @since : 2024/8/17
 **/
public class AccountDomainService implements TransferAbility{
    private static final List<AccountEntity> record = new ArrayList<>();
    static {
        AccountEntity account_1 = new AccountEntity(1L, BigDecimal.valueOf(1000));
        AccountEntity account_2 = new AccountEntity(2L, BigDecimal.valueOf(1000));
        record.add(account_1);
        record.add(account_2);
    }

    public AccountEntity getById(Long id){
        Map<Long, AccountEntity> accountEntityMap = record.stream()
                .collect(Collectors.toMap(AccountEntity::getId, Function.identity()));
        return accountEntityMap.get(id);
    }

    @Override
    /**
     * 转账能力,从账户A 发起转账到 账户B
     */
    public void transferTo(AccountEntity fromAccount, AccountEntity toAccount, BigDecimal transferAmount) {
        fromAccount.withdraw(transferAmount);
        toAccount.receive(transferAmount);
    }
}

提款、收款、转账接口

public interface ReceivingAbility {
    boolean receive(BigDecimal amount);
}

public interface TransferAbility {
    /** 转账能力 */
    void transferTo(AccountEntity fromAccount, AccountEntity toAccount, BigDecimal transferAmount);
}

public interface WithdrawAbility {
    boolean withdraw(BigDecimal withDrawAmount);
}

转账测试类,从账户A中转500元到账户B中

import org.example.transfer.AccountDomainService;
import org.example.transfer.AccountEntity;
import org.junit.Before;
import org.junit.Test;

import java.math.BigDecimal;
import java.util.Random;

/**
 * @author : kenny
 * @since : 2024/8/17
 **/
public class AccountGateWayTest {
    private AccountDomainService accountDomainService;

    @Before
    public void init(){
        accountDomainService = new AccountDomainService();
    }

    @Test
    public void test_TransferAmount(){
        AccountEntity fromAccount = accountDomainService.getById(1L);
        AccountEntity toAccount = accountDomainService.getById(2L);

        for (int i = 0; i < 5; i++){
            // 执行五次转账
            accountDomainService.transferTo(fromAccount, toAccount, BigDecimal.valueOf(30));
        }

        System.out.println("账号ID:" + fromAccount.getId() + ",转账后余额" + fromAccount.getBalance());
        System.out.println("账号ID:" + toAccount.getId() + ",转账后余额" + toAccount.getBalance());
    }
}

执行转账

/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/bin/java -Dvisualvm.id=878818195126083 -ea -Didea.test.cyclic.buffer.size=1048576 -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56664:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit5-rt.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/tools.jar:/Users/kenny/Documents/CodeProjects/Courses/Example/java-thread-example/target/test-classes:/Users/kenny/Documents/CodeProjects/Courses/Example/java-thread-example/target/classes:/Users/kenny/.m2/repository/junit/junit/4.13.2/junit-4.13.2.jar:/Users/kenny/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit4 AccountGateWayTest,test_TransferAmount
账号ID:1,转账后余额850
账号ID:2,转账后余额1150

Process finished with exit code 0

从转账的案例来看,转账之间所有的操作,必须是要么全部成功,要么全部失败

否则当账户A提款之后,在转账过程中发生了异常,如账户B的发生了某些收款异常,那账户A的钱就莫名其妙的被扣减了。

在单线程下,要保证操作的原子性(数据持久化在数据库中时),我们可以对转账操作加一个事务,比如Spring开发中,在方法生命中添加**@Transactional** 注解,这样就保证了操作原子性。

有时候,我们可能会使用多线程提高性能,,这个时候就需要对余额进行加锁了,同一时刻只能有一个线程操作余额,否则就会造成更新丢失的问题。

package org.example.transfer;

import java.math.BigDecimal;

/** 账户实体
 * @author : kenny
 * @since : 2024/8/17
 **/
public class AccountEntity implements WithdrawAbility, ReceivingAbility{
    private Long id;
    private BigDecimal balance;
    private final Object wLock = new Object();

    public AccountEntity(Long id, BigDecimal balance) {
        this.id = id;
        this.balance = balance;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public BigDecimal getBalance() {
        return balance;
    }

    @Override
    public boolean withdraw(BigDecimal withDrawAmount) {
        synchronized (wLock){
            if (withDrawAmount == null || withDrawAmount.compareTo(BigDecimal.valueOf(0)) <= 0){
                throw new RuntimeException("当前账户" + id +  " " + "提取金额不能为0");
            }

            if (balance == null || balance.compareTo(BigDecimal.valueOf(0)) <= 0){
                throw new RuntimeException("当前账户" + id +  " " + "账户余额不足");
            }

            BigDecimal afterBalance = balance.subtract(withDrawAmount);
            if (afterBalance.compareTo(BigDecimal.valueOf(0)) <= 0){
                throw new RuntimeException("当前账户" + id +  " " + "账户余额不足");
            }

            balance = afterBalance;
            return true;
        }

    }

    @Override
    public boolean receive(BigDecimal amount) {
        synchronized (wLock){
            if (amount == null || amount.compareTo(BigDecimal.valueOf(0)) <= 0){
                throw new RuntimeException("当前账户" + id +  " " +"收款异常");
            }

            balance = balance.add(amount);
            return true;
        }
    }
}

可见性

可见性是指当多个线程同时访问同一个变量时,其中一个线程对该变量的修改能够被其他线程立即看到。

public class AccountEntity implements WithdrawAbility, ReceivingAbility{
    private Long id;
    private volatile BigDecimal balance;
    private final Object wLock = new Object();   
}

Java提供了 volatile 关键字来保证可见性, 另外,由于synchronized和Lock的保证了同一时刻只能有一个线程(互斥)进入到临界区,所以也能保证可见性。

有序性

即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,允许编译器和处理器对指令进行重排序,在单线程程序中下,重排序并不会影响代码的执行结果,但在多线程并发程序执行过程中,就会产生意想不到的结果。

竟态条件(check-then-act)

我理解的竞态条件为,当我们写了一个类,并且包含了对静态变量或者实例的访问、修改,如果不对代码块进行加锁同步,那就会产生竟态条件

案例一:单例模式引发的竟态条件。

public class SingleInstance {
    
    public static Object instance;
  
    public Object getINSTANCE(){
        if (instance == null){
            return new Object();
        }
        
        return instance;
    }
}

A、B线程同时执行**getINSTANCE()方法,A线程读取到instance为null,生成了一个新的对象。与此同时,B线程也读取到了instance为null,也生成了一个新的对象。那么两次getINSTANCE()** 执行返回的对象就不是同一个。

案例二:i++ 引发的竟态条件

i++ 是一个非原子性的操作,对于它而言,它的操作分为3个步骤,读取原值,写入值,改回原值

public class UnSafeCount {
    public static int i = 0;

    public static void main(String[] args) {
        i++;
        System.out.println(i);
    }
}
    
    
// 反编译后的字节码
// class version 52.0 (52)
// access flags 0x21
public class org/example/UnSafeCount {

  // compiled from: UnSafeCount.java

  // access flags 0x9
  public static I i

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lorg/example/UnSafeCount; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 13 L0
    GETSTATIC org/example/UnSafeCount.i : I
    ICONST_1
    IADD
    PUTSTATIC org/example/UnSafeCount.i : I
   L1
    LINENUMBER 14 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    GETSTATIC org/example/UnSafeCount.i : I
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L2
    LINENUMBER 15 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 10 L0
    ICONST_0
    PUTSTATIC org/example/UnSafeCount.i : I
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
}

通过反编译字节码会发现其中对于 i 的操作会有三条指令,分别是 **ICONST_0、ISTORE 1、IINC 1 1,**在多线程的处理的情况下,如果对i同时进行操作,那就可能会造成最终i的值产生不正确的结果

结论:发生竟态条件的原因是由于多个线程执行的时序问题,导致程序逻辑和我们预期不一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值