我在下面的某处读到了。
Java volatile关键字并不意味着原子,它常见的误解 在声明volatile之后,++
操作将是原子的 操作原子,你仍然需要确保使用独占访问 Java中的synchronized
方法或块。
那么如果两个线程同时攻击volatile
原始变量会发生什么?
这是否意味着无论谁锁定它,都将首先设置其值。如果在此期间,当第一个线程更改其值时,某些其他线程出现并读取旧值,那么新线程是否会读取其旧值?
Atomic和volatile关键字有什么区别?
答案 0 :(得分:118)
volatile
关键字的效果大致是对该变量的每个单独读取或写入操作都是原子的。
然而,值得注意的是,需要多个读/写的操作(例如i++
,相当于i = i + 1
,执行一次读取和一次写入 - 不是原子,因为另一个线程可以在读和写之间写入i
。
Atomic
类,如AtomicInteger
和AtomicReference
,以原子方式提供更多种类的操作,具体包括AtomicInteger
的增量。
答案 1 :(得分:63)
挥发性和原子性是两个不同的概念。 Volatile确保在不同的线程中某个预期(内存)状态为真,而Atomics确保对变量的操作以原子方式执行。
在Java中使用以下两个线程的示例:
主题A:
value = 1;
done = true;
主题B:
if (done)
System.out.println(value);
从value = 0
和done = false
开始,线程规则告诉我们,线程B是否打印值是未定义的。 此外值在此时也未定义!要解释这一点,您需要了解一下Java内存管理(可能很复杂),简而言之:线程可能会创建变量的本地副本,并且JVM可以对代码进行重新排序以对其进行优化,因此无法保证上述代码按照该顺序运行。设置为true并将然后设置值设置为1可能是JIT优化的可能结果。
volatile
只能确保在访问此类变量时,新值将立即显示给所有其他线程和执行顺序确保代码处于您期望的状态。因此,对于上面的代码,将done
定义为 volatile 将确保每当线程B检查变量时,它都是false或true,如果是,则value
1}}也被设置为1。
作为 volatile 的副作用,这种变量的值在线程范围内以原子方式设置(执行速度非常小)。然而,这对于i.E.的32位系统来说非常重要。使用长(64位)变量(或类似),在大多数其他情况下,设置/读取变量无论如何都是原子的。但原子访问和原子操作之间存在重要区别。 Volatile仅确保访问是原子的,而Atomics确保操作是原子的。
采用以下示例:
i = i + 1;
无论你如何定义i,在执行上述行时读取值的另一个Thread可能会得到i或i + 1,因为操作不是原子的。如果另一个线程将i设置为不同的值,在最坏的情况下,我可以将其设置回线程A之前的任何值,因为它只是在基于旧值计算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这样的Atomics确保这种操作以原子方式发生。所以上面的问题不可能发生,一旦两个线程完成,我将是1000或1001。
答案 2 :(得分:44)
多线程环境中有两个重要概念。
Volatile
根除了可见性问题,但它没有处理原子性问题。 Volatile
将阻止编译器重新排序涉及写入和随后读取volatile变量的指令。例如k++
这里k++
不是单个机器指令,而是三个机器指令。
所以即使你将变量声明为volatile
,它也不会使这个操作成为原子,这意味着另一个线程可以看到一个中间结果,这是另一个线程的陈旧或不需要的值。
但是AtomicInteger
,AtomicReference
基于Compare and swap instruction。 CAS有三个操作数:要操作的内存位置V
,预期的旧值A
和新值B
。 CAS
原子地将V
更新为新值B
,但前提是V
中的值与预期的旧值A
匹配;否则它什么都不做。在任何一种情况下,它都会返回当前V
中的值。 JVM在AtomicInteger
,AtomicReference
中使用它,它们将函数称为compareAndSet()
。如果底层处理器不支持此功能,则JVM通过spin lock实现它。
答案 3 :(得分:21)
正如所示尝试,volatile
仅处理可见性。
在并发环境中考虑此代码段:
boolean isStopped = false;
:
:
while (!isStopped) {
// do some kind of work
}
这里的想法是,某些线程可以将isStopped
的值从false更改为true,以便向后续循环指示是时候停止循环。
直观地说,没有问题。逻辑上,如果另一个线程使isStopped
等于true,则循环必须终止。实际情况是,即使另一个线程使isStopped
等于true,循环也可能永远不会终止。
原因并不直观,但考虑到现代处理器具有多个内核,并且每个内核都有多个寄存器和多个级别的高速缓存,其他处理器无法访问。换句话说,在一个处理器的本地内存中缓存的值对于在不同处理器上执行的线程不可见。这是并发的核心问题之一:可见性。
Java内存模型无法保证何时对线程中的变量所做的更改可能对其他线程可见。为了保证更新在发布后立即可见,您必须同步。
volatile
关键字是一种弱同步形式。虽然它对互斥或原子性没有任何作用,但它确实提供了保证,一个线程中对变量的更改一旦生成就会对其他线程可见。因为对Java中非8字节变量的单独读取和写入是原子的,所以声明变量volatile
提供了一种简单的机制,可以在没有其他原子性或互斥要求的情况下提供可见性。
答案 4 :(得分:13)
使用volatile
关键字:
long
和double
。 (所有其他的,原始访问已经保证是原子的!) java.util.concurrent.atomic.*
类是java docs:
支持无锁线程安全的类的小工具包 对单个变量进行编程。本质上,这个类 package扩展了volatile值,字段和数组的概念 那些也提供原子条件更新的元素 操作形式:
boolean compareAndSet(expectedValue, updateValue);
原子类是围绕原子compareAndSet(...)
函数构建的,该函数映射到原子CPU指令。原子类引入了发生在之前的顺序,就像volatile
变量一样。 (有一个例外:weakCompareAndSet(...)
)。
来自java docs:
当线程看到由a引起的原子变量的更新时 weakCompareAndSet,它不一定会看到任何其他更新 在weakCompareAndSet之前发生的变量。
问题:
这是否意味着无论谁锁定它,都会设置 它的价值第一。如果在此期间,其他一些线程出现了 在第一个线程更改其值时读取旧值,然后不更改 新线程会读取它的旧值吗?
你没有锁定任何东西,你所描述的是一种典型的竞争条件,如果线程在没有正确同步的情况下访问共享数据,它最终会发生。如前所述,在这种情况下声明变量volatile
只会确保其他线程会看到变量的更改(该值不会缓存在某个缓存的寄存器中,只能由一个线程看到)。 / p>
AtomicInteger
和volatile int
之间的区别是什么?
AtomicInteger
在int
上提供适当同步的原子操作(例如incrementAndGet()
,getAndAdd(...)
,...),volatile int
只会确保int
对其他线程的可见性。
答案 5 :(得分:11)
那么如果两个线程同时攻击一个易失性原始变量会发生什么呢?
通常每个人都可以增加值。但是有时,两者都会同时更新值,而不是递增2,而是增加1并且只增加1。
这是否意味着无论谁锁定它,都将首先设置其值。
没有锁定。这就是synchronized
的用途。
如果在此期间,当第一个线程正在更改其值时,某些其他线程会出现并读取旧值,那么新线程是否会读取其旧值?
是,
Atomic和volatile关键字有什么区别?
AtomicXxxx包含一个volatile,因此它们基本相同,不同之处在于它提供了更高级别的操作,例如用于实现增量的CompareAndSwap。
AtomicXxxx还支持lazySet。这就像一个易失性集,但不会使管道停止等待写完成。这可能意味着,如果你读到一个你刚写的值,你可能会看到旧值,但你不应该这样做。不同之处在于设置volatile需要大约5 ns,bit lazySet大约需要0.5 ns。