写点什么

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

  • 2024-08-17
    上海
  • 本文字数:6490 字

    阅读完需:约 21 分钟

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++ 是一个非原子性的操作,什么是非原子性操作呢?即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

对于 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 0x21public 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 的值产生不正确的结果

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

发布于: 刚刚阅读数: 3
用户头像

给我一个bug,或者一个hug 2020-08-26 加入

还未添加个人简介

评论

发布
暂无评论
Java多线程中原子性、可见性、有序性以及竟态条件案例_Java_Geek漫游指南_InfoQ写作社区