Java中的易失性与静态

Posted

技术标签:

【中文标题】Java中的易失性与静态【英文标题】:Volatile vs Static in Java 【发布时间】:2011-01-26 06:54:40 【问题描述】:

static 表示所有对象的值的一份副本,volatile 表示所有线程的值的一份副本是否正确?

无论如何static 变量值也将是所有线程的一个值,那我们为什么要选择volatile

【问题讨论】:

volatile官方解释:cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile 【参考方案1】:

在Java中声明一个static变量,意味着无论创建多少类对象,都只会有一个副本。即使根本没有创建Objects,也可以访问该变量。但是,线程可能有它的本地缓存值。

当变量是 volatile 而不是 static 时,每个 Object 将有一个变量。所以,表面上看起来与普通变量没有区别,但与静态完全不同。但是,即使有Object 字段,线程也可以在本地缓存变量值。

这意味着如果两个线程同时更新同一 Object 的变量,并且该变量未声明为 volatile,则可能存在其中一个线程缓存旧值的情况。

即使您通过多个线程访问 静态 值,每个线程也可以拥有其本地缓存副本!为避免这种情况,您可以将变量声明为 static volatile,这将强制线程每次读取全局值。

但是,volatile 不能替代正确的同步! 例如:

private static volatile int counter = 0;

private void concurrentMethodWrong() 
  counter = counter + 5;
  //do something
  counter = counter - 5;

同时多次执行concurrentMethodWrong可能会导致counter的最终值不为零! 要解决这个问题,你必须实现一个锁:

private static final Object counterLock = new Object();

private static volatile int counter = 0;

private void concurrentMethodRight() 
  synchronized (counterLock) 
    counter = counter + 5;
  
  //do something
  synchronized (counterLock) 
    counter = counter - 5;
  

或者使用AtomicInteger 类。

【讨论】:

volatile 修饰符保证读取字段的任何线程都会看到最近写入的值,因此如果变量在多个线程之间共享并且您需要此功能,则它是必要的,这取决于您的用例. 当您说“本地缓存”时,缓存是什么? CPU 缓存,某种 JVM 缓存? @mertinan 是的,该变量可以位于更靠近处理器或内核的缓存中。有关详细信息,请参阅cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html。 'volatile' not 是否暗示“每个对象一个变量”。没有“静态”可以做到这一点。 -1 未能澄清 OP 的这一基本误解。 @EJP 我认为“将变量声明为易失性,每个对象都会有一个变量。所以表面上看起来与普通变量没有区别”这句话解释了这一点,我添加了而不是静态的,请随时编辑文章并改进措辞以使其更清晰。【参考方案2】:

静态和易失的区别:

静态变量:如果两个线程(假设t1t2)正在访问同一个对象并更新一个声明为静态的变量,那么这意味着t1t2可以在各自的缓存中制作同一对象(包括静态变量)的本地副本,因此t1对其本地缓存中的静态变量所做的更新不会反映在t2缓存的静态变量中。

静态变量用于对象的上下文,其中一个对象所做的更新将反映在同一类的所有其他对象中但不在线程的上下文中其中一个线程更新到静态变量将立即将更改反映到所有线程(在它们的本地缓存中)。

易失性变量:如果两个线程(假设t1t2)正在访问同一个对象并更新一个声明为易失性的变量,那么这意味着t1t2可以制作自己的 Object 本地缓存 除了声明为 volatile 的变量 。所以 volatile 变量将只有一个主副本,将由不同的线程更新,一个线程对 volatile 变量的更新将立即反映到另一个线程。

【讨论】:

你好@Som,如果我错了,请纠正我。但是您不认为语句“但不在 Thread 的上下文中,其中一个线程更新到静态变量将立即将更改反映到所有线程(在它们的本地缓存中)。”应该是“但不在 Thread 的上下文中,其中一个线程对静态变量的更新将 > 立即将更改反映到所有线程(在它们的本地缓存中)。” @Jaikrat 是的,这让我很困惑。我的理解是你是对的,而这个答案是错误的,正如所写的那样。如果我错了,我也希望得到纠正。 @Jaikrat 线程虽然不会缓存静态变量,但请参考更新后的静态变量。 @Som 那么你想更正 para 并删除 但不是在 Thread 的上下文中。那很混乱。谢谢 很遗憾,这个答案是不正确的。在现代 CPU 上,即使是 volatile 变量也可以在不同的 CPU 缓存之间共享。这没有问题,因为缓存会在修改缓存行之前协商其独占所有权。【参考方案3】:

除了其他答案,我想为它添加一张图片(图片易于理解)

static 变量可以为单个线程缓存。在多线程环境中如果一个线程修改了它的缓存数据,这可能不会反映到其他线程,因为它们有一个副本

volatile 声明确保线程不会缓存数据并仅使用共享副本

image source

【讨论】:

静态变量在线程下的对象之间共享?这应该读取静态变量在对象之间共享所有对象,无论线程如何。 "易失性变量在多个线程之间共享(对象也是如此)。" Volatile 不会改变变量在多个线程或对象之间共享的方式。它改变了运行时缓存值的方式。 您对静态变量的评论也适用于非静态变量,并且“将被缓存”和“不会反映”可能应该改写为“可能被缓存”和“可能不会反映”。 我很困惑。这张照片解决了我所有的问题!【参考方案4】:

我认为staticvolatile 根本没有关系。我建议你阅读java教程了解Atomic Access,以及为什么要使用原子访问,了解interleaved是什么,你会找到答案。

【讨论】:

【参考方案5】:

简单来说,

    static : static 变量与相关联,而不是任何对象。类的每个实例共享一个类变量,该变量位于内存中的一个固定位置

    volatile:此关键字适用于 classinstance 变量。

使用 volatile 变量可降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与随后读取该相同变量建立起先发生关系。这意味着对 volatile 变量的更改始终对其他线程可见

看看Javin Paul 的这个article 以更好地理解易失性变量。

在没有volatile关键字的情况下,每个线程栈中变量的值可能不同。通过将变量设为volatile,所有线程将在其工作内存中获得相同的值,并且避免了内存一致性错误。

这里的术语variable 可以是static(类)变量或instance(对象)变量。

关于您的查询:

不管怎样,一个静态变量值也将是所有线程的一个值,那我们为什么要选择 volatile 呢?

如果我的应用程序中需要instance 变量,我不能使用static 变量。即使在static 变量的情况下,由于线程缓存,也无法保证一致性,如图所示。

使用volatile 变量可降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取同一变量建立起先发生关系。这意味着对 volatile 变量的更改始终对其他线程可见。

更重要的是,这也意味着当一个线程读取一个 volatile 变量时,它不仅看到了对 volatile 的最新更改,还看到了导致更改的代码的副作用 => 使用 volatile 变量仍然可能出现内存一致性错误。为避免副作用,您必须使用同步变量。但是java中有更好的解决方案。

使用简单的原子变量访问比通过同步代码访问这些变量更有效

java.util.concurrent 包中的一些类提供了不依赖于同步的原子方法。

有关详细信息,请参阅此high level concurrency control 文章。

特别是看看Atomic variables。

相关的 SE 问题:

Volatile Vs Atomic

Volatile boolean vs AtomicBoolean

Difference between volatile and synchronized in Java

【讨论】:

我非常感谢这个答案。我之前知道volatile 是什么,但是,这个答案让我明白了很多,为什么我仍然需要将volatilestatic 变量一起使用。 volatile:此关键字适用于类和实例变量。你上面所说的关于适用于类的说法是不正确的。只有两个适用于变量的关键词是volatile和transient。所以易变的不适用于课程。 volatile 适用于类(静态)变量。查看google中的双锁单例链接,你会发现你的理解是错误的。 ***.com/questions/18093735/… private static volatile 是有效的声明。【参考方案6】:

易失性变量值访问将直接从主存储器。它应该只在多线程环境中使用。 静态变量将被加载一次。如果它在单线程环境中使用,即使变量的副本将被更新,访问它也不会因为只有一个线程而造成伤害。

现在,如果在多线程环境中使用静态变量,那么如果期望从中得到期望的结果,就会出现问题。由于每个线程都有自己的副本,因此一个线程对静态变量的任何增量或减量都可能不会反映在另一个线程中。

如果人们期望从静态变量中得到想要的结果,那么在多线程中使用 volatile 和静态,那么一切都会得到解决。

【讨论】:

【参考方案7】:

不确定静态变量是否缓存在线程本地内存中。但是当我执行两个线程(T1,T2)访问同一个对象(obj)时,当 T1 线程对静态变量进行更新时,它会反映在 T2 中。

【讨论】:

【参考方案8】:

如果我们将变量声明为静态变量,则该变量将只有一个副本。 因此,每当不同的线程访问该变量时,该变量将只有一个最终值(因为该变量只分配了一个内存位置)。

如果一个变量被声明为 volatile,所有线程都会有自己的变量副本,但值是从主存中获取的。所以,所有线程中的变量值都是一样的。

因此,在这两种情况下,要点是变量的值在所有线程中都是相同的。

【讨论】:

如果一个变量被声明为 volatile,所有线程都将拥有自己的变量副本,但该值取自主内存。 => 对。 所以,变量在所有线程中的值都是一样的。 => 错了,每个线程都会对同一个Object使用相同的值,但是每个Object都会有自己的副本。

以上是关于Java中的易失性与静态的主要内容,如果未能解决你的问题,请参考以下文章

优化volatile变量

与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?

Teradata 中易失性表的光标

多线程中的Java volatile关键字

Java 内存模型:易失性变量和发生前

译STM32L4x6系列用户手册第四章 - 防火墙(FireWall)