你在Java中使用volatile关键字吗?

Ric*_*ard 628 java multithreading volatile keyword

在今天的工作中,我遇到了volatileJava中的关键字.我不太熟悉它,我发现了这个解释:

Java理论与实践:管理波动性

鉴于该文章解释了相关关键字的详细信息,您是否使用过它,或者您是否曾经看到过以正确方式使用此关键字的情况?

Gre*_*tes 711

volatile具有内存可见性的语义.基本上,在volatile写入操作完成后,所有读取器(特别是其他线程)都可以看到字段的值.没有volatile,读者可以看到一些非更新的价值.

回答你的问题:是的,我使用一个volatile变量来控制某些代码是否继续循环.循环测试该volatile值并继续(如果是)true.可以false通过调用"停止"方法来设置条件.循环看到了false在stop方法完成执行后测试值时,并终止.

我强烈推荐的" Java Concurrency in Practice "这本书给出了很好的解释volatile.本书由撰写问题中引用的IBM文章的同一人撰写(事实上,他引用了该文章底部的书).我的用法volatile是他的文章称之为"模式1状态标志".

如果您想了解更多有关如何volatile工作的内容,请阅读Java内存模型.如果你想超越这个级别,请查看一本好的计算机体系结构书籍,如Hennessy和Patterson,并阅读缓存一致性和缓存一致性.

  • 这个答案是正确的,但不完整.它忽略了JSR 133中定义的新Java内存模型带来的`volatile`的一个重要属性:当一个线程读取一个`volatile`变量时,它不仅会看到其他线程最后写入的值,而且还会看到所有其他写入"volatile"写入时在该另一个线程中可见的其他变量.见[本答案](http://stackoverflow.com/questions/8769570/volatile-piggyback-is-this-enough-for-visiblity/8769692#8769692)和[本参考文献](http://www.ibm. COM/developerworks的/库/ J-jtp03304 /#2.0). (108认同)
  • 对于初学者,我会要求您演示一些代码(请?) (43认同)
  • 问题中链接的文章有代码示例. (6认同)
  • @fefrei:“立即”是一个俗语。当然,如果没有实际指定执行时间和线程调度算法,就无法保证。程序找出易失性读取是否在特定易失性写入之后的唯一方法是检查查看值是否为预期的已写入值。 (2认同)

And*_*NER 170

"... volatile修饰符保证读取字段的任何线程都会看到最近写入的值." - Josh Bloch

如果您正在考虑使用volatile,请阅读java.util.concurrent处理原子行为的包.

关于Singleton模式的维基百科帖子显示了使用中的不稳定性.

  • 为什么同时存在`volatile`和`synchronized`关键字? (15认同)
  • 关于Singleton模式的维基百科文章已经发生了很大变化,并且不再具有所谓的"volatile"示例.它可以在[存档版本]中找到(https://en.wikipedia.org/w/index.php?title=Singleton_pattern&oldid=214082674#Java_5_solution). (5认同)
  • @ptkato这两个关键字具有完全不同的目的,因此这个问题作为比较没有多大意义,尽管它们都与并发相关。这就像在说“为什么同时存在‘void’和‘public’关键字”。 (3认同)

Pre*_*raj 118

关键点volatile:

  1. 同步在Java中使用Java关键字的可能synchronizedvolatile和锁.
  2. 在Java中,我们不能有synchronized变量.将synchronized关键字与变量一起使用是非法的,将导致编译错误.synchronized您可以使用java volatile变量代替在Java中使用变量,该变量将指示JVM线程volatile从主内存读取变量的值,而不是在本地缓存它.
  3. 如果多个线程之间没有共享变量,则无需使用该volatile关键字.

资源

用法示例volatile:

public class Singleton {
    private static volatile Singleton _instance; // volatile variable
    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null)
                    _instance = new Singleton();
            }
        }
        return _instance;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们正在第一次请求时懒洋洋地创建实例.

如果我们不创建_instance变量volatile,则创建实例的Thread Singleton无法与其他线程通信.因此,如果线程A正在创建Singleton实例,并且在创建之后,CPU会破坏等,所有其他线程将无法看到值为_instance非null,并且他们将认为它仍然被赋值为null.

为什么会这样?因为读取器线程没有进行任何锁定,并且在写入器线程从同步块中出来之前,内存将不会被同步,并且_instance将不会在主内存中更新值.使用Java中的Volatile关键字,这由Java本身处理,并且所有读取器线程都可以看到此类更新.

结论:volatile关键字也用于在线程之间传递内存的内容.

不使用volatile的示例用法:

public class Singleton{    
    private static Singleton _instance;   //without volatile variable
    public static Singleton getInstance(){   
          if(_instance == null){  
              synchronized(Singleton.class){  
               if(_instance == null) _instance = new Singleton(); 
      } 
     }   
    return _instance;  
    }
Run Code Online (Sandbox Code Playgroud)

上面的代码不是线程安全的.虽然它在synchronized块中再次检查实例的值(出于性能原因),但JIT编译器可以重新排列字节码,方式是在构造函数完成执行之前设置对实例的引用.这意味着方法getInstance()返回一个可能尚未完全初始化的对象.为了使代码具有线程安全性,可以使用关键字volatile,因为Java 5用于实例变量.标记为volatile的变量只有在对象的构造函数完全执行后才能对其他线程可见.
资源

在此输入图像描述

volatile在Java中的用法:

快速失败的迭代器通常使用volatile列表对象上的计数器实现.

  • 更新列表后,计数器会递增.
  • Iterator被创建时,计数器的当前值被嵌入Iterator对象.
  • Iterator执行操作时,该方法比较两个计数器值并抛出ConcurrentModificationException它们是否不同.

故障安全迭代器的实现通常是轻量级的.它们通常依赖于特定列表实现的数据结构的属性.没有一般模式.

  • “故障快速迭代器通常使用易失性计数器来实现”-不再如此,太昂贵了:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6625725 (2认同)
  • 为了线程安全,也可以使用“private static final Singleton _instance;”。 (2认同)

Pyr*_*cal 50

volatile对于停止线程非常有用.

并不是说你应该编写自己的线程,Java 1.6有很多很好的线程池.但如果你确定需要一个线程,你需要知道如何阻止它.

我用于线程的模式是:

public class Foo extends Thread {
  private volatile boolean close = false;
  public void run() {
    while(!close) {
      // do work
    }
  }
  public void close() {
    close = true;
    // interrupt here if needed
  }
}
Run Code Online (Sandbox Code Playgroud)

注意不需要同步

  • @Jori,你需要volatile,因为while循环中的线程读取与调用close()的线程不同.如果没有volatile,运行循环的线程可能永远不会看到要关闭的更改. (25认同)
  • 我想知道为什么这甚至是必要的.如果其他线程必须以这样的方式对线程的状态更改作出反应,以至于线程同步处于危险之中,那是不是唯一必要的? (2认同)
  • @Pyrolistical-您是否观察到线程_never_看到实践中的更改?还是可以扩展示例以可靠地触发该问题?我很好奇,因为我知道我已经使用(并看到其他人在使用)与示例基本相同的代码,但没有使用volatile关键字,并且它似乎总是可以正常工作。 (2认同)
  • @aroth:对于今天的JVM,您可以观察到在实践中,即使使用最简单的示例,您也无法*可靠地*重现此行为.对于更复杂的应用程序,您有时会在代码中使用内存可见性保证的其他操作使其正常工作,这尤其危险,因为您不知道它为什么有效并且代码中的简单,明显无关的更改可能会破坏您的应用… (2认同)

Dav*_* L. 30

使用的一个常见示例volatile是使用volatile boolean变量作为标志来终止线程.如果你已经启动了一个线程,并且你希望能够安全地从另一个线程中断它,你可以让线程定期检查一个标志.要停止它,请将标志设置为true.通过创建标志volatile,您可以确保正在检查它的线程将在下次检查它时设置它,而不必使用synchronized块.


Sup*_*hne 19

volatile关键字声明的变量有两个主要特性使它变得特殊.

  1. 如果我们有一个volatile变量,它就不能被任何线程缓存到计算机的(微处理器)高速缓存中.访问总是发生在主内存中.

  2. 如果对volatile变量进行写操作,并且突然请求读操作,则保证在读操作之前完成写操作.

以上两个品质推断出这一点

  • 读取volatile变量的所有线程肯定会读取最新值.因为没有缓存的值可以污染它.并且只有在当前写操作完成后才会授予读请求.

而另一方面,

  • 如果我们进一步调查我提到的#2,我们可以看到volatile关键字是维护共享变量的理想方式,该共享变量具有'n'个读线程并且只有一个写线程来访问它.一旦我们添加了volatile关键字,就完成了.关于线程安全没有任何其他开销.

Conversly,

我们不能volatile单独使用关键字来满足一个共享变量,该变量有多个写入线程访问它.

  • 这解释了易失性和同步之间的区别。 (4认同)
  • 遗憾的是,这是不正确的。“Volatile”不控制缓存,也不为其他CPU的内存视图提供任何神奇的即时全局更新。“易失性”只是确保每当完成对变量的引用(读取或写入)时,JVM 都会执行对虚拟内存空间中变量分配地址的引用,而不是对存储在寄存器或其他某个位置的值的引用。方便优化器选择影子位置(如栈),也不跳过优化器判断上的参考。 (2认同)
  • 如果没有“易失性”,诸如“for (...) {a += b + c;}”之类的指令可能根本不会引用内存位置,只是将“a”、“b”和“c”保留在寄存器中循环的整个持续时间。当 CPU 将一个值写入虚拟内存地址(或者相应的物理内存地址)时,更新不会立即对其他 CPU 可见,也不会立即刷新到 RAM [*]。 (2认同)
  • 更新只是简单地放入本地CPU的缓存中,然后排队到实现内存一致性协议(例如MESI)的CPU间互连中,协议消息开始传输到其他CPU,最终导致它们的缓存被更新也。这需要很短但非零的时间。与此同时,其他 CPU 仍然不知道更新已经发生。如果 CPU1 更新了易失性变量 X,并且 CPU2 稍后读取它,则 CPU2 可能会找到 X 的旧值或 X 的新值。 (2认同)
  • 在写入方面,“易失性”和“非易失性”之间的区别在于,对于“易失性”CPU2 将在纳秒左右看到更新,而对于“非易失性”,更新延迟是不可预测的,并且取决于优化器。在读取方面,不同之处在于,对于“易失性”,程序代码中对变量的引用强制引用虚拟内存中分配的变量位置。而对于“非易失性”,优化器可以选择跳过进行此类引用。 (2认同)
  • 确保读取变量的“绝对最新”值的唯一方法是通过互锁操作,即通过锁定(“synchronized”、ReenterantLock 等)或使用互锁操作类(如 AtomicInterger)。 (2认同)
  • [*] Flusing to RAM 无论如何都没有多大意义,因为 CPU 不能直接看到 RAM,而只能通过其本地缓存系统看到 RAM。当缓存系统丢失某个位置时,它不需要从 RAM 中填充它:它可能知道该位置保存在其他 CPU 缓存中(由该 CPU 拥有),因此不会从 RAM 中读取数据,而是请求它通过互连的缓存一致性协议从其他 CPU 的缓存中获取。 (2认同)

小智 13

没有人提到长和双变量类型的读写操作的处理.读取和写入是参考变量和大多数原始变量的原子操作,但长变量和双变量类型除外,它们必须使用volatile关键字作为原子操作.@链接

  • @KaiWang您不需要出于原子性而在布尔值上使用volatile。但是您当然可能出于可见性原因。那是你想说的吗? (2认同)

yka*_*ich 12

是的,只要您希望多个线程访问可变变量,就必须使用volatile.它不是很常见的用例,因为通常您需要执行多个单独的原子操作(例如,在修改之前检查变量状态),在这种情况下,您将使用synchronized块.


Ani*_*kur 9

除了停止使用volatile关键字的线程之外,IMO有两个重要的场景

  1. 双重检查锁定机制.经常用于Singleton设计模式.在这__CODE__.
  2. 虚假的唤醒.即使没有发出通知呼叫,线程有时也会从等待呼叫中唤醒.这种行为称为supurious wakeup.这可以通过使用条件变量(布尔标志)来抵消.只要标志为真,就将wait()调用放入while循环中.因此,如果由于除了notify/notifyall之外的任何原因线程从等待调用中唤醒,那么它遇到标志仍然是真的,因此再次调用wait.在调用notify之前,将此标志设置为true.在这种情况下__CODE__.


Ami*_*yli 9

假设某个线程修改了共享变量的值(如果您没有volatile对该变量使用修饰符)。当其他线程想要读取该变量的值时,它们看不到更新后的值,因为它们从 CPU 的缓存而不是 RAM 内存中读取变量的值。此问题也称为Visibility Problem

通过声明共享变量volatile,对计数器变量的所有写入都将立即写回主内存。此外,计数器变量的所有读取都将直接从主存储器读取。

public class SharedObject {
    public volatile int sharedVariable = 0;
}
Run Code Online (Sandbox Code Playgroud)

对于非易失性变量,无法保证 Java 虚拟机 (JVM) 何时将数据从主内存读取到 CPU 缓存,或将数据从 CPU 缓存写入主内存。这可能会导致几个问题,我将在以下各节中解释这些问题。


例子:

想象一下这样的情况,两个或多个线程可以访问包含如下声明的计数器变量的共享对象:

public class SharedObject {
    public int counter = 0;
}
Run Code Online (Sandbox Code Playgroud)

还可以想象一下,只有线程 1 递增计数器变量,但线程 1 和线程 2 都可能不时读取计数器变量。

如果计数器变量未声明为 易失性,则无法保证计数器变量的值何时从 CPU 缓存写回主内存。这意味着,CPU 缓存中的计数器变量值可能与主内存中的不同。这种情况如下所示:

易挥发的

由于变量尚未被另一个线程写回主内存,因此线程看不到变量的最新值的问题称为“可见性”问题。一个线程的更新对其他线程不可见。


yoA*_*ex5 7

volatile=> synchronized[关于]

volatile对于程序员来说,该值始终是最新的。问题是该值可以保存在不同类型的硬件内存中。例如它可以是 CPU 寄存器、CPU 缓存、RAM... ?PU 寄存器和 CPU 缓存属于 CPU,不能共享数据,不像 RAM 在多线程环境中救援

在此处输入图片说明

volatile关键字表示将直接从/向 RAM 内存读取和写入变量。它有一些计算足迹

Java 5volatile通过支持扩展happens-before[关于]

对 volatile 字段的写入发生在对该字段的每次后续读取之前。

Read is after write
Run Code Online (Sandbox Code Playgroud)

volatile关键字不能治愈一个race condition情况,当多个线程可以同时一些值。答案是synchronized关键词[关于]

因此,仅当一个线程写入而其他线程仅读取该volatile值时才安全


Rud*_*nto 5

如果您正在开发多线程应用程序,则需要使用'volatile'关键字或'synchronized'以及您可能拥有的任何其他并发控制工具和技术.这种应用的示例是桌面应用.

如果您正在开发将部署到应用程序服务器(Tomcat,JBoss AS,Glassfish等)的应用程序,则您不必自己处理并发控制,因为应用程序服务器已经解决了这一问题.事实上,如果我记得正确,Java EE标准禁止在servlet和EJB中进行任何并发控制,因为它是"基础"层的一部分,你应该放弃它来处理它.如果要实现单例对象,则只在此类应用程序中执行并发控制.如果您使用像Spring这样的框架编织组件,这甚至已经解决了.

因此,在Java开发的大多数情况下,应用程序是Web应用程序并使用IoC框架(如Spring或EJB),您不需要使用"volatile".


fat*_*kin 5

volatile只保证所有线程,甚至自己都在递增.例如:计数器同时看到变量的同一面.它不是用来代替同步或原子或其他东西,它完全使读取同步.请不要将它与其他java关键字进行比较.如下面的示例所示,易失性变量操作也是原子的,它们会立即失败或成功.

package io.netty.example.telnet;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static volatile  int a = 0;
    public static void main(String args[]) throws InterruptedException{

        List<Thread> list = new  ArrayList<Thread>();
        for(int i = 0 ; i<11 ;i++){
            list.add(new Pojo());
        }

        for (Thread thread : list) {
            thread.start();
        }

        Thread.sleep(20000);
        System.out.println(a);
    }
}
class Pojo extends Thread{
    int a = 10001;
    public void run() {
        while(a-->0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.a++;
            System.out.println("a = "+Main.a);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

即使你把挥发性或不结果总是不同的.但是如果您使用AtomicInteger,则结果将始终相同.这与同步也是一样的.

    package io.netty.example.telnet;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;

    public class Main {

        public static volatile  AtomicInteger a = new AtomicInteger(0);
        public static void main(String args[]) throws InterruptedException{

            List<Thread> list = new  ArrayList<Thread>();
            for(int i = 0 ; i<11 ;i++){
                list.add(new Pojo());
            }

            for (Thread thread : list) {
                thread.start();
            }

            Thread.sleep(20000);
            System.out.println(a.get());

        }
    }
    class Pojo extends Thread{
        int a = 10001;
        public void run() {
            while(a-->0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Main.a.incrementAndGet();
                System.out.println("a = "+Main.a);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)