java - Java中volatile和synchronized的区别

标签 java multithreading java-me synchronized volatile

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

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

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

...

  • access to a volatile variable never has the potential to block: we're only ever doing a simple read or write, so unlike a synchronized block we will never hold on to any lock;
  • because accessing a volatile variable never holds a lock, it is not suitable for cases where we want to read-update-write as an atomic operation (unless we're prepared to "miss an update");


它们是什么意思 读-更新-写 ?是不是写也是更新或者他们只是意味着更新 是依赖于读取的写入?

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

最佳答案

了解线程安全有两个方面很重要。

  • 执行控制,以及
  • 内存可见性

  • 第一个与控制代码何时执行(包括执行指令的顺序)以及它是否可以并发执行有关,第二个与其他线程何时可以看到内存中已完成操作的效果有关。因为每个 CPU 在它和主内存之间都有几个级别的缓存,运行在不同 CPU 或内核上的线程可以在任何给定的时间看到不同的“内存”,因为线程被允许获取和工作在主内存的私有(private)副本上。

    使用 synchronized阻止任何其他线程获取监视器(或锁)对于同一对象 ,从而防止所有受同步保护的代码块 在同一对象上 从并发执行。同步还创建了一个“发生在之前”的内存屏障,导致内存可见性约束,使得在某个线程释放锁之前所做的任何事情都会出现在另一个随后获取 的线程中。同锁在它获得锁之前发生。实际上,在当前的硬件上,这通常会导致在获取监视器时刷新 CPU 缓存并在释放时写入主内存,这两者(相对)都很昂贵。

    使用 volatile ,另一方面,强制所有对 volatile 变量的访问(读或写)发生在主内存中,有效地将 volatile 变量保持在 CPU 缓存之外。这对于某些仅要求变量的可见性正确且访问顺序不重要的操作非常有用。使用 volatile也改变了 long 的处理方式和 double要求对它们的访问是原子的;在某些(较旧的)硬件上,这可能需要锁定,但在现代 64 位硬件上则不需要。在 Java 5+ 的新 (JSR-133) 内存模型下, volatile 的语义已得到加强,在内存可见性和指令排序方面几乎与同步一样强(请参阅 http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile )。出于可见性的目的,对 volatile 字段的每次访问都相当于半个同步。

    Under the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire. In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.

    -- JSR 133 (Java Memory Model) FAQ



    因此,现在两种形式的内存屏障(在当前 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() 方法未同步,两个线程可能会同时进入它。在可能发生的许多排列中,一个是线程 1 对 counter==1000 进行测试并发现它为真,然后被挂起。然后线程 2 执行相同的测试,也看到它为真并被挂起。然后线程 1 恢复并将计数器设置为 0。然后线程 2 恢复并再次将计数器设置为 0,因为它错过了线程 1 的更新。即使线程切换没有像我所描述的那样发生,这也可能发生,但这仅仅是因为两个不同的计数器缓存副本存在于两个不同的 CPU 内核中,并且每个线程都在一个单独的内核上运行。就此而言,一个线程可能具有一个值的计数器,而另一个线程可能由于缓存而具有某个完全不同的值。

    在这个例子中,重要的是变量 counter 从主内存读取到缓存中,在缓存中更新,并且只在以后某个不确定的点在内存屏障发生或缓存内存需要其他东西时写回主内存。制作柜台volatile对这段代码的线程安全性来说是不够的,因为对最大值和赋值的测试是离散操作,包括增量是一组非原子read+increment+write机器指令,例如:
    MOV EAX,counter
    INC EAX
    MOV counter,EAX
    

    可变变量仅在 时有用全部 对它们执行的操作是“原子的”,例如我的示例,其中对完全形成的对象的引用只能读取或写入(实际上,通常它只从一个点写入)。另一个示例是支持写时复制列表的 volatile 数组引用,前提是该数组仅通过首先获取对它的引用的本地副本来读取。

    关于java - Java中volatile和synchronized的区别,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3519664/

    相关文章:

    java - Java多线程程序中生产者消费者线程的互换角色

    java - Java 中的线程池究竟在做什么?

    java - 哪些手机支持哪种 J2ME(Java 微型版)规范?

    blackberry - 如何在 Blackberry 项目中添加外部 jar 或 zip 文件

    java - Spring 切换用户 : only allow a user with a specific role switch to another user with specific roles

    java - Google 集合中 BiMap 的精髓

    java - 将 'HttpOnly' 属性添加到所有 session cookie

    java - 根据命令提示符的输入打印星星

    java - 以下情况是否需要同步?

    blackberry - J2ME 应用程序不适用于诺基亚 n81 和三星 f330 等,但适用于 BB 和诺基亚 n97 等