内存中的原子性

简介: 原子性是不可分割的,操作也是不能打断的。接下来我们一起来看看怎么在多线程中保证原子性吧。天天学习,天天进步!

内存中的原子性

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

原子性

原子性:不可分割,操作不能打断。

我们上节在对内存可见性造成影响的代码中讲述了可以使用volatile来保证内存的可见性,当工作内存的值变化了会立即刷新到主内存中,同时使其他线程的工作内存的值失效,从而从主内存中重新拉取新的值。那么多线程同时操作一个值,仅仅只有可见性,能得到我们预期的结果吗?

写个例子来看一下

示例

package com.wangscaler.jmm;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */
​
public class Atomicity {
    volatile int num = 0;
​
    public void add() {
        this.num++;
    }
​
    public static void main(String[] args) {
        Atomicity atomicity = new Atomicity();
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicity.add();
            }
        }, "Add").start();
        new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicity.add();
            }
        }, "Add2").start();
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("两个线程结束后最终的num值为" + atomicity.num);
    }
}

正常来说,我们预期的结果应该是200000。然而当我们执行的时候却总是是少于这个数的,当然也是有几率达到预期值。为什么执行的结果大出意料呢?

this.num++;的字节码如下(IDEA查看字节码,可参考往期文章从字节码讲解i++和++i的区别|8月更文挑战):

 2 getfield #2 <com/wangscaler/jmm/Atomicity.num : I>
 5 iconst_1
 6 iadd
 7 putfield #2 <com/wangscaler/jmm/Atomicity.num : I>

getfield从主内存读取num的值,iconst_1将值放到操作数栈位置一的位置,iadd进行++操作,putfield写回主内存。在多线程中,这四步中间可能会和其他线程交替执行。

  • 假设当主内存为10的时候,线程Add读取了主内存的num的值10(Add执行字节码getfield)
  • 紧接着Add2的线程也读取了主内存num的值10(Add2执行字节码getfield)
  • 然后两个线程分别进行了+1操作(Add先执行字节码iadd,随后Add2执行字节码iadd),因为工作内存的数据变化了,又分别写入主内存。
  • 假设线程Add先写入11;其后线程Add2也写入了11。(Add先执行字节码putfield,随后Add2执行字节码putfield)
  • 这时就出现了我们非预期的结果,我们预期的是经过两个线程之后,值应该为12,现在的结果却是11。
  • 多次出现这种情况那么最终的值肯定是低于200000。

如何解决原子性问题

以下的修改三次修改均是在上面示例的基础上进行修改的。

1、原子操作类(CAS)

num的类型由int修改为AtomicInteger;将add方法的num++;修改为num.getAndIncrement();如下所示:

package com.wangscaler.jmm;
​
import java.util.concurrent.atomic.AtomicInteger;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */
​
public class Atomicity {
    AtomicInteger num = new AtomicInteger();
​
    public void add() {
        num.getAndIncrement();
    }
​
}

为什么AtomicInteger会保证原子性呢,我们打开源码发现

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

继续查看getAndAddInt的源码

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
​
    return var5;
}

在这个getAndAddInt方法里是个循环,循环判断主存的值V和你预期的值A一样时,才会允许你将新值B写入主存,否则重新循环在主存获取值再次比较。(具体的细节可翻阅CAS,后期我会专门写什么是CAS,CAS操作包含三个操作数 :内存位置(V)、预期原值(A)和新值(B)。)

除了double、long之外的所有基本类型的读取或赋值也都是原子性操作。但是读取并赋值的操作不是原子性的例如我们常见的i++就不是原子性操作。

2、synchronized

给add方法使用关键字synchronized修饰。

public synchronized void add() {
    this.num++;
}

使用这个关键字之后,就保证了操作不能打断。也就是说一个线程执行add方法时,需要等待add的所有字节码执行完之后,下一个线程才能执行。

  • 当Add线程获得add方法的执行权之后,其他线程Add2执行到add方法时将阻塞。
  • Add线程从主内存读取num,进行++操作,写回主内存
  • Add2线程才能获得add方法的执行权。

从而保证了num的值达到我们预期的效果。synchronized就是给Add方法加了一把锁,所以我们也可以自己去实现这个锁。

3、Lock锁

package com.wangscaler.jmm;
​
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
 * @author WangScaler
 * @date 2021/8/4 13:49
 */
​
public class Atomicity {
    volatile int num = 0;
    Lock addLock = new ReentrantLock();
​
    public void add() {
        addLock.lock();
        try {
            num++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            addLock.unlock();
        }
    }
}

和synchronized一样我们给add方法加了一把锁,这样谁获取到权限谁才能执行。

总结

在保持原子性上,优先使用原子操作类(CAS),因为他是非阻塞的同步机制的乐观锁,而synchronized、Lock两种加锁的机制是阻塞的,是一种悲观的互斥锁,大大影响我们的效率。

目录
相关文章
|
8天前
|
缓存 Java 程序员
Java内存模型深度解析:可见性、有序性和原子性
在多线程编程中,正确理解Java内存模型对于编写高效且无bug的并行程序至关重要。本文将深入探讨JMM的三大核心特性:可见性、有序性和原子性,并结合实例分析如何利用这些特性来避免常见的并发问题。
7 1
|
6天前
|
Java
Java内存模型之原子性问题
Java内存模型之原子性问题
|
12月前
|
存储 缓存 SpringCloudAlibaba
JUC并发编程(一):Java内存模型(JMM)及三大特性:可见性、有序性、原子性
在当今高流量、高并发的互联网业务场景下,**并发编程技术**显得尤为重要,不管是哪一门编程语言,掌握并发编程技术是个人进阶的必经之路。时隔一个半月没有写技术博客文章,有点生疏了。。。闲话少叙,接下来我将围绕并发编程知识点进行总结讲解,这里从并发编程入门开始,讲述Java内存模型和并发的三大特性。
138 1
JUC并发编程(一):Java内存模型(JMM)及三大特性:可见性、有序性、原子性
|
缓存 Java
Java内存模型小析之原子性和可见性(二)
在上篇文章中我们简单的说了一下jvm的内存布局(点这里查看),在这篇文章中我们继续java内存模型方面的东西。 原子性 注意这里的原子性不是数据库事务中的原子性。
1387 0
|
18天前
|
消息中间件 存储 Kafka
实时计算 Flink版产品使用问题之 从Kafka读取数据,并与两个仅在任务启动时读取一次的维度表进行内连接(inner join)时,如果没有匹配到的数据会被直接丢弃还是会被存储在内存中
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
10天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
22 2
|
14天前
|
存储
数据在内存中的存储(2)
数据在内存中的存储(2)
25 5
|
14天前
|
存储 小程序 编译器
数据在内存中的存储(1)
数据在内存中的存储(1)
28 5
|
14天前
|
存储 安全 Java
SpringSecurity6从入门到实战之初始用户如何存储到内存
Spring Security 在 SpringBoot 应用中默认使用 `UserDetailsServiceAutoConfiguration` 类将用户信息存储到内存中。当classpath有`AuthenticationManager`、存在`ObjectPostProcessor`实例且无特定安全bean时,此配置生效。`inMemoryUserDetailsManager()`方法创建内存用户,通过`UserDetails`对象填充`InMemoryUserDetailsManager`的内部map。若要持久化到数据库,需自定义`UserDetailsService`接口实