2024-03-23  阅读(49)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/6213961023

回答

所谓原子性,就是一个操作或者多个操作,要么完全执行,要么完全不执行,在执行过程中是不能被其他因素打断或者插入。

volatile 我们知道它能保证可见性和有序性的,但是对于原子性,它无法保证。

详细分析

原子性

原子性的核心概念就两次:完整、不可分割。也就是某个线程在正在做某个业务时,要么它这个业务完整执行完,要么完全不执行,不允许处于一个中间状态。

我们最最经典的原子操作是银行账户转账:从账户 A 向账户 B 转入 10000 元,它只允许存在两种情况:

  1. 转账成功:从账户 A 扣掉 10000 元,给账户 B 增加 10000 元。
  2. 转账失败:账户 A 、账户 B 均没有变化。

不可能存在一个所谓的中间状态,如从账户 A 扣掉 10000 元,但是账户 B 不增加 10000 元。

我们看下面这段代码:

i = 1;       // 1
j = i;       // 2
i++;         // 3
i = j + 1;   // 4

这四句代码中哪些是原子操作,哪些不是,首先在单线程环境下,我们可以认为上面四个全部都是原子操作,但是多线程,这四句代码情况如下:

  1. i = 1;:是原子操作。赋值操作通常是原子的,因为它只涉及单个步骤,即将一个值写入到一个变量中。
  2. j = i;:不是原子操作。因为这段可以分为两个步骤,读取 i,赋值 j。
  3. i++;:不是原子操作,它包含了三个步骤:读取 i,执行 i + 1,新值写入 i。
  4. i = j + 1:与 i++ 一样,包含了三个步骤。

其实在多线程环境下,Java 只保证基本类型的赋值操作是原子性,但这也仅限于单个变量且没有涉及到 64 位的 longdouble 类型。

volatile 能保证原子性吗?

我们先看一个简单的例子:

public class SkTest {
    private int num = 0;

    public void addNum() {
        num++;
    }

    public int getNum() {
        return num;
    }

    public static void main(String[] args) {
        SkTest test = new SkTest();
        for (int i = 0; i < 20; i++) {
            new Thread(() ->{
                for (int j = 0; j < 10000; j++) {
                    test.addNum();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println("num 的值为:" + test.getNum());
    }
}

num 是普通变量,num++ 又不是原子操作,那么在多线程环境下,肯定不是线程安全的,我们执行5次,结果分别为:

num 的值为:108691
num 的值为:46885
num 的值为:135318
num 的值为:45002
num 的值为:34058

那在 num 前面加上 volatile 呢?

public class SkTest {
    private volatile int num = 0;
    
    // 省略其他代码
}

执行 5 次结果:

num 的值为:65806
num 的值为:115472
num 的值为:73228
num 的值为:109038
num 的值为:114220

为什么会出现这种情况呢?还是 volatile 的语句决定的,volatile 语义如下:

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。 当读一个 volatile变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

即保证可见性,但不保证原子性。大明哥用一张图来解释下上图示例你就清晰了:

JMM 对原子性问题的保证

  • 在Java中,对基本数据类型的变量读取赋值操作是原子性操作,但是 long 和 double 是非原子性协定,在某些场景下,它并不是原子性的。
  • 锁的原子性保证:在Java中,锁(synchronized 或者 lock 锁)可以保证在同一时刻只有一个线程能执行一个同步块的代码。它保证了在同步块内的操作是原子性的,对共享变量的所有读取和写入都是原子性的。
  • Atomic类的原子操作:Java 提供了一套原子类,如AtomicIntegerAtomicLong等,它们利用了CAS 操作来实现变量操作的原子性。
阅读全文
  • 点赞