挥发性 VS 原子能

我在下面读到过。

关键字并不意味着原子,这是它常见的误解 在声明易失性之后,++操作将是原子操作,以使 您仍然需要使用操作原子来确保独占访问 Java 中的 synchronized方法或块。

那么,如果两个线程同时攻击一个 volatile基本变量会发生什么情况呢?

这是否意味着无论谁锁定了它,都将首先设定它的价值。如果同时,其他线程出现并读取旧值而第一个线程改变了它的值,那么新线程不会读取它的旧值吗?

原子关键字和易失性关键字的区别是什么?

157123 次浏览

volatile关键字的作用大致是使该变量上的每个单独的读或写操作对所有线程都是原子可见的。

然而,值得注意的是,一个需要多个读/写操作的操作——例如 i++,它等价于执行一个读和一个写操作的 i = i + 1——是 没有原子操作,因为另一个线程可能在读和写之间写到 i

AtomicIntegerAtomicReference这样的 Atomic类原子地提供了更多种类的操作,特别是包括 AtomicInteger的增量。

多线程环境中有两个重要的概念:

  1. 原子性
  2. 能见度

volatile关键字根除了可见性问题,但是它不处理原子性。volatile会防止编译器重新排序涉及写入和后续读取 Volatile变量的指令,例如 k++。 在这里,k++不是一个单一的指令,而是三个:

  1. 将该值复制到登记册;
  2. 增值;
  3. 放回去。

因此,即使您将一个变量声明为 volatile,这也不会使这个操作成为原子操作; 这意味着另一个线程可以看到一个中间结果,这个中间结果对于另一个线程来说是一个过时的或不需要的值。

另一方面,AtomicReference4、 AtomicReference5是以 AtomicReference6为基础的。CAS 有三个操作数: 要操作的内存位置 V、预期的旧值 A和新值 BCASV自动更新为新值 B,但仅当 V中的值与预期的旧值 A匹配时才更新 V; 否则,它将不执行任何操作。在这两种情况下,它都返回当前在 V中的值。AtomicIntegerAtomicReferenceAtomicReference1方法利用了这个功能,如果底层处理器支持这个功能; 如果不支持,那么 JVM 通过 AtomicReference7实现它。

如所示,volatile只处理可见性。

考虑一下并发环境中的这个片段:

boolean isStopped = false;
:
:


while (!isStopped) {
// do some kind of work
}

这里的想法是,某个线程可以将 isStopped的值从 false 更改为 true,以便向后面的循环指示是时候停止循环了。

直觉上来说,没有问题。逻辑上,如果另一个线程使 isStopped等于 true,那么循环必须终止。实际上,即使另一个线程使 isStopped等于 true,循环也可能永远不会终止。

其原因并不直观,但考虑到现代处理器有多个内核,而且每个内核都有多个寄存器和多级缓存内存(不能被其他处理器访问)。换句话说,缓存在一个处理器的本地内存中的值是在另一个处理器上执行的线程的 看不见。这就是并发的核心问题之一: 可见性。

Java 内存模型不能保证什么时候对一个线程中的变量所做的更改可以对其他线程可见。为了保证更新一完成就可见,必须进行同步。

volatile关键字是同步的一种弱形式。虽然它不支持互斥锁或原子性,但它确实提供了一个保证,即一旦对一个线程中的变量进行了更改,其他线程就可以看到它。由于对非8字节变量的单独读写在 Java 中是原子性的,因此声明变量 volatile提供了一种简单的机制,可以在没有其他原子性或互斥锁需求的情况下提供可见性。

那么,如果两个线程同时攻击一个易失性原语变量,会发生什么情况呢?

通常每一个都可以增加值。但是有时候,两个线程都会同时更新值,而不是总共增加2个,两个线程都增加1个,并且只增加1个。

这是否意味着无论谁锁定了它,都将首先设定它的价值。

没有锁,这就是 synchronized的作用。

同时,如果其他线程出现并读取旧值而第一个线程改变了它的值,那么新线程不会读取旧值吗?

是的,

原子关键字和易失性关键字的区别是什么?

AtomicXxxx 包装了一个易失性,所以它们基本上是相同的,区别在于它提供了更高级别的操作,比如用于实现增量的 CompareAndSwap。

AtomicXxxx 也支持 lazySet。这就像一个易失性集合,但是不会使管道停止,等待写操作完成。这可能意味着,如果读取刚刚写入的值,可能会看到旧值,但是无论如何都不应该这样做。不同的是,设置一个易失性需要大约5ns,位延迟设置需要大约0.5 ns。

挥发性和原子性是两个不同的概念。Volative 确保跨不同线程的某个预期(内存)状态是真实的,而 Atomics 确保对变量的操作是自动执行的。

以 Java 中的两个线程为例:

主题 A:

value = 1;
done = true;

线程 B:

if (done)
System.out.println(value);

value = 0done = false开始,线程规则告诉我们,线程 B 是否会打印值是未定义的。为了解释这一点,你需要了解一点 Java 内存管理(这可能很复杂) ,简而言之: 线程可能创建变量的本地副本,JVM 可以重新排序代码来优化它,因此不能保证上面的代码正是按照这个顺序运行的。将设置值设置为 true 和 那么设置值设置为1可能是 JIT 优化的一个可能结果。

volatile只是确保,在访问这样一个变量的时刻,新的值将立即对执行顺序确保的所有其他线程 还有可见,代码处于您所期望的状态。因此,对于上面的代码,将 done定义为 反复无常将确保每当线程 B 检查变量时,它要么为 false,要么为 true,如果为 true,那么 value也被设置为1。

作为 反复无常的一个副作用,这样一个变量的值是在线程范围内自动设置的(执行速度的代价非常小)。然而,只有在使用长(64位)变量(或类似变量)的32位系统中,这一点才是重要的,在大多数情况下,设置/读取变量无论如何都是原子的。但是在原子访问和原子操作之间有一个重要的区别。Volative 仅确保访问是原子的,而 Atomics 确保 行动是原子的。

以下面这个例子为例:

i = i + 1;

不管您如何定义 i,在执行上面的行时读取值的另一个线程可能会得到 i 或 i + 1,因为 行动不是原子的。如果另一个线程将 i 设置为不同的值,在最坏的情况下,线程 A 可以将 i 设置回之前的值,因为它正在基于旧值计算 i + 1,然后再将 i 设置为旧值 + 1。说明:

Assume i = 0
Thread A reads i, calculates i+1, which is 1
Thread B sets i to 1000 and returns
Thread A now sets i to the result of the operation, which is i = 1

像 AtomicInteger 这样的原子确保这样的操作以原子方式发生。所以上面的问题不可能发生,一旦两个线程都完成,我可能是1000或1001。

使用 volatile关键字:

  • 使非原子64位操作成为原子操作: longdouble(所有其他原始访问都已经保证是原子的!)
  • 保证其他线程可以看到变量更新 + 可见性效果: 在写入 Volatile变量之后,所有在写入变量之前可见的变量在读取相同的 Volatile变量之后(发生-排序之前)对另一个线程可见。

根据 Java 文件java.util.concurrent.atomic.*课程是:

支持无锁线程安全的类的小型工具包 在单个变量上进行编程 包扩展了易失性值、字段和数组的概念 元素转换为那些也提供原子条件更新的元素 表格的运作:

boolean compareAndSet(expectedValue, updateValue);

原子类是围绕映射到原子 CPU 指令的原子 compareAndSet(...)函数构建的。原子类像 volatile变量一样引入 以前发生过排序。(只有一个例外: weakCompareAndSet(...))。

来自 java 文档:

当线程看到由 它不一定看到任何其他 在弱 CompareAndSet 之前发生的变量。

回答你的问题:

这是否意味着无论谁锁定了它,它都将被设置 它的价值第一。在此期间,一些其他的线索出现了 读取旧值,而第一个线程正在更改其值,然后不读取旧值 新线程会读取它的旧值吗?

你不锁定任何东西,你所描述的是一个典型的竞争条件,最终会发生,如果线程访问共享数据没有适当的同步。如前所述,在本例中声明变量 volatile只能确保其他线程看到变量的更改(该值不会缓存在某个缓存的寄存器中,只有一个线程可以看到)。

AtomicIntegervolatile int有什么不同?

AtomicInteger通过适当的同步提供 int上的原子操作(例如。incrementAndGet()getAndAdd(...),...) ,volatile int只是确保了 int对其他线程的可见性。