Java 多线程中原子性、可见性、有序性以及竟态条件案例
- 2024-08-17 上海
本文字数:6490 字
阅读完需:约 21 分钟
原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
代码案例
用一个简单的银行账户转账收款的代码例子来看,其中包含了账户实体模型,实现了提款、收款接口
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 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 的值产生不正确的结果
结论:发生竟态条件的原因是由于多个线程执行的时序问题,导致程序逻辑和我们预期不一致。
版权声明: 本文为 InfoQ 作者【Geek漫游指南】的原创文章。
原文链接:【http://xie.infoq.cn/article/2e360a69a52648293ca13881e】。文章转载请联系作者。
Geek漫游指南
给我一个bug,或者一个hug 2020-08-26 加入
还未添加个人简介
评论