首页 文章

Java中volatile和synchronized之间的区别

提问于
浏览
189

我想知道将变量声明为 volatile 并始终访问Java中 synchronized(this) 块中的变量之间的区别?

根据这篇文章http://www.javamex.com/tutorials/synchronization_volatile.shtml,有很多要说的,有许多不同之处,但也有一些相似之处 .

我对这条信息特别感兴趣:

...访问volatile变量永远不会阻塞:我们只进行简单的读或写操作,因此与synchronized块不同,我们永远不会保留任何锁;因为访问volatile变量永远不会持有锁,所以它不适合我们想要读取update-write作为原子操作的情况(除非我们准备“错过更新”);

read-update-write 是什么意思?写入也不是更新,还是仅仅意味着 update 是一个依赖于读取的写入?

最重要的是,何时更适合声明变量 volatile 而不是通过 synchronized 块访问它们?对于依赖于输入的变量,使用 volatile 是一个好主意吗?例如,有一个名为 render 的变量,它通过渲染循环读取并由按键事件设置?

5 回答

  • 341

    重要的是要理解线程安全有两个方面 .

    • 执行控制,和

    • 内存可见性

    第一个与控制何时执行代码(包括执行指令的顺序)以及它是否可以并发执行有关,第二个与内存中对已完成内容的影响何时对其他线程可见有关 . 因为每个CPU在它和主内存之间有几级缓存,所以在不同CPU或内核上运行的线程在任何给定时刻都可以看到“内存”不同,因为允许线程获取并处理主内存的私有副本 .

    使用 synchronized 可防止任何其他线程获取监视器(或锁定) for the same object ,从而防止同步 on the same object 保护的所有代码块同时执行 . 同步还会创建一个内存屏障,导致内存可见性约束,以至于某些线程释放锁定所做的任何操作都会显示给另一个线程,后来在获取锁定之前已经发生了 the same lock . 实际上,在当前硬件上,这通常导致在获取监视器时刷新CPU高速缓存并在释放时写入主存储器,这两者都是(相对)昂贵的 .

    另一方面,使用 volatile 强制对主存储器进行对volatile变量的所有访问(读或写),从而有效地将volatile变量保留在CPU缓存之外 . 这对于一些只需要变量的可见性正确且访问顺序不重要的操作非常有用 . 使用 volatile 也会更改 longdouble 的处理,以要求对它们的访问是原子的;在某些(较旧的)硬件上,这可能需要锁定,但不适用于现代64位硬件 . 在Java 5的新(JSR-133)内存模型下,volatile的语义已经被强化,几乎与内存可见性和指令排序同步一样强(参见http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile) . 出于可见性的目的,对易失性字段的每次访问都像半同步一样 .

    在新的内存模型下,挥发性变量无法相互重新排序 . 不同之处在于现在不再那么容易重新排序它们周围的正常字段访问 . 写入易失性字段与监视器释放具有相同的记忆效应,从易失性字段读取具有与监视器获取相同的记忆效应 . 实际上,因为新的存储器模型对具有其他字段访问(易失性或非易失性)的易失性字段访问的重新排序施加了更严格的约束,所以当线程A写入易失性字段f时线程A可见的任何内容在读取f时对线程B可见 . - JSR 133(Java内存模型)常见问题解答

    因此,现在两种形式的内存屏障(在当前的JMM下)都会导致指令重新排序障碍,从而阻止编译器或运行时跨屏障重新排序指令 . 在旧的JMM中,volatile并没有阻止重新排序 . 这可能很重要,因为除了内存障碍之外,唯一的限制是,对于任何特定的线程,代码的净效果与它相同如果指令是按照它们在源中出现的顺序执行的话 .

    volatile的一个用途是在运行时重新创建共享但不可变的对象,许多其他线程在其执行周期的特定点处引用该对象 . 一旦发布,需要其他线程开始使用重新创建的对象,但不需要额外的完全同步开销以及随之而来的争用和缓存刷新 .

    // Declaration
    public class SharedLocation {
        static public SomeObject someObject=new SomeObject(); // default object
        }
    
    // Publishing code
    // Note: do not simply use SharedLocation.someObject.xxx(), since although
    //       someObject will be internally consistent for xxx(), a subsequent 
    //       call to yyy() might be inconsistent with xxx() if the object was 
    //       replaced in between calls.
    SharedLocation.someObject=new SomeObject(...); // new object is published
    
    // Using code
    private String getError() {
        SomeObject myCopy=SharedLocation.someObject; // gets current copy
        ...
        int cod=myCopy.getErrorCode();
        String txt=myCopy.getErrorText();
        return (cod+" - "+txt);
        }
    // And so on, with myCopy always in a consistent state within and across calls
    // Eventually we will return to the code that gets the current SomeObject.
    

    请特别说明您的读 - 更新 - 写问题 . 考虑以下不安全的代码:

    public void updateCounter() {
        if(counter==1000) { counter=0; }
        else              { counter++; }
        }
    

    现在,在updateCounter()方法未同步的情况下,两个线程可以同时输入它 . 在可能发生的许多排列中,一个是thread-1对counter == 1000进行测试并发现它为真,然后被暂停 . 然后thread-2执行相同的测试,并且看到它是真的并且被暂停 . 然后thread-1恢复并将counter设置为0.然后thread-2恢复并再次将counter设置为0,因为它错过了thread-1的更新 . 即使没有像我所描述的那样发生线程切换,也会发生这种情况,但仅仅因为两个不同CPU核心中存在两个不同的计数器缓存副本,并且每个线程都运行在一个单独的核心上 . 就此而言,一个线程可以在一个值处具有计数器而另一个线程可以仅仅因为缓存而具有一些完全不同的值 .

    在这个例子中重要的是变量计数器从主存储器读入高速缓存,在高速缓存中更新,并且仅在发生内存屏障时或者在其他情况下需要高速缓存存储器时才在某个不确定点写回主存储器 . 使计数器 volatile 不足以保证此代码的线程安全,因为最大值和赋值的测试是离散操作,包括增量,这是一组非原子机器指令,如:

    MOV EAX,counter
    INC EAX
    MOV counter,EAX
    

    只有当对它们执行的 all 操作是"atomic"时,易失性变量才有用,例如我的例子中只读取或写入对完全形成的对象的引用(实际上,它通常只从单个点写入) . 另一个例子是支持写时复制列表的易失性数组引用,前提是只能通过首先获取对它的引用的本地副本来读取数组 .

  • 91

    volatile是一个字段修饰符,而synchronized则修改代码块和方法 . 因此,我们可以使用这两个关键字指定一个简单访问器的三个变体:int i1;
    int geti1(){return i1;}

    volatile int i2;
    int geti2(){return i2;}

    int i3;
    synchronized int geti3(){return i3;}
    geti1()访问当前线程中当前存储在i1中的值 . 线程可以有变量的本地副本,并且数据不必与其他线程中保存的数据相同 . 特别是,另一个线程可能在其线程中更新了i1,但当前线程中的值可能与更新的 Value . 事实上,Java有一个“主”内存的概念,这是保存变量当前“正确”值的内存 . 线程可以拥有自己的变量数据副本,并且线程副本可以与“主”内存不同 . 所以实际上,对于i1,“main”存储器的值可能为1,如果thread1和thread2都已更新,则对于i1,thread1的值为2,对于i1,则对于i1,值为3 i1但这些更新的值尚未传播到“主”内存或其他线程 . 另一方面,geti2()有效地从“主”存储器访问i2的值 . 不允许volatile变量具有与“main”内存中当前保存的值不同的变量的本地副本 . 实际上,声明为volatile的变量必须使其数据在所有线程之间同步,因此无论何时在任何线程中访问或更新变量,所有其他线程都会立即看到相同的值 . 通常,volatile变量比“plain”变量具有更高的访问和更新开销 . 通常,允许线程拥有自己的数据副本以提高效率 . volitile和synchronized之间有两个不同之处 . 首先同步获取并释放监视器上的锁定,这些锁定一次只能强制一个线程执行代码块 . 这是同步的众所周知的方面 . 但同步也同步内存 . 实际上是同步的将整个线程内存与“主”内存同步 . 因此执行geti3()会执行以下操作:线程获取监视器上的对象锁定 . 线程存储器刷新其所有变量,即它的所有变量都有效地从“主”存储器读取 . 执行代码块(在这种情况下,将返回值设置为i3的当前值,该值可能刚刚从“main”存储器复位) . (对变量的任何更改现在通常都会写入“main”内存,但对于geti3(),我们没有更改 . )线程在监视器上释放对象的锁定 . 因此,volatile只在线程内存和“主”内存之间同步一个变量的值,synchronized会同步线程内存和“主”内存之间所有变量的值,并锁定和释放监视器以启动 . 明确同步可能比volatile更具开销 .

    http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

  • 2

    synchronized 是方法级/块级访问限制修饰符 . 它将确保一个线程拥有临界区的锁 . 只有拥有锁的线程才能进入 synchronized 块 . 如果其他线程正在尝试访问此关键部分,则必须等到当前所有者释放锁定 .

    volatile 是变量访问修饰符,它强制所有线程从主内存中获取变量的最新值 . 访问 volatile 变量不需要锁定 . 所有线程可以同时访问volatile变量值 .

    使用volatile变量的一个很好的例子: Date variable .

    假设您已创建日期变量 volatile . 访问此变量的所有线程始终从主存储器获取最新数据,以便所有线程显示实际(实际)日期值 . 您不需要为同一变量显示不同时间的不同线程 . 所有线程都应显示正确的日期值 .

    enter image description here

    看看这个article,以便更好地理解 volatile 概念 .

    Lawrence Dol cleary解释了你的 read-write-update query .

    关于你的其他疑问

    什么时候更适合声明变量volatile而不是通过synchronized访问它们?

    如果您认为所有线程都应实时获得变量的实际值,则必须使用 volatile ,就像我为Date变量解释的示例一样 .

    对于依赖输入的变量使用volatile是个好主意吗?

    答案与第一次查询中的答案相同 .

    有关更好的理解,请参阅此article .

  • 17

    java中的

    • volatile 关键字是字段修饰符,而 synchronized 修改代码块和方法 .

    • synchronized 获取并释放监视器上的锁定java volatile 关键字不需要 .

    • synchronized 的情况下,可以阻止Java中的线程等待任何监视器,而Java中的 volatile 关键字则不然 .

    • synchronized 方法在Java中影响性能超过 volatile 关键字 .

    • 由于Java中的 volatile 关键字仅同步线程内存和"main"内存之间的一个变量的值,而 synchronized 关键字同步线程内存和"main"内存之间的所有变量的值,并锁定并释放要启动的监视器 . 由于这个原因,Java中的 synchronized 关键字可能比 volatile 具有更多的开销 .

    • 您无法在null对象上进行同步,但java中的 volatile 变量可能为null .

    • 从Java 5写入 volatile 字段与监视器版本具有相同的记忆效应,从易失性字段读取具有与监视器获取相同的记忆效应

  • 7

    我喜欢jenkov's的解释

    Visibility of Shared Objects

    如果两个或多个线程共享一个对象,而没有正确使用 volatile 声明或 synchronization ,则一个线程对共享对象的更新可能对其他线程不可见 .

    想象一下,共享对象最初存储在主存储器中 . 然后,在CPU上运行的线程将共享对象读入其CPU缓存中 . 它在那里对共享对象进行了更改 . 只要CPU缓存尚未刷新回主内存,共享对象的更改版本对于在其他CPU上运行的线程是不可见的 . 这样,每个线程最终都可能拥有自己的共享对象副本,每个副本都位于不同的CPU缓存中 .

    下图说明了草绘的情况 . 在左CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其count变量更改为2.此更改不是对于在正确的CPU上运行的其他线程可见,因为计数更新尚未刷新回主内存 .

    enter image description here

    要解决此问题,您可以使用Java's volatile keyword . volatile关键字可以确保直接从主内存读取给定变量,并在更新时始终写回主内存 .

    Race Conditions

    如果两个或多个线程共享一个对象,并且多个线程更新该共享对象中的变量,则可能会出现竞争条件 .

    想象一下,如果线程A将共享对象的变量计数读入其CPU缓存中 . 想象一下,线程B也做同样的事情,但是进入不同的CPU缓存 . 现在,线程A将一个添加到count,而线程B执行相同的操作 . 现在var1已经增加了两次,每个CPU缓存一次 .

    如果这些增量按顺序执行,则变量计数将增加两次并将原始值2写回主存储器 .

    但是,两个增量同时执行而没有适当的同步 . 无论线程A和B中哪一个将其更新后的计数版本写回主存储器,更新的值将仅比原始值高1,尽管有两个增量 .

    该图说明了如上所述的竞争条件问题的发生:

    enter image description here

    要解决此问题,您可以使用Java synchronized block . 同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分 . 同步块还保证在同步块内访问的所有变量都将从主存储器中读入,当线程退出同步块时,所有更新的变量将再次刷新回主存储器,无论变量是声明为volatile还是不 .

相关问题