Java 中 volatile 和 synchronized 的区别

新手上路,请多包涵

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

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

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

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

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

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

原文由 Albus Dumbledore 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 792
2 个回答

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

  1. 执行控制,以及
  2. 内存可见性

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

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

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

在新的内存模型下,volatile 变量不能相互重新排序仍然是事实。不同之处在于,现在不再那么容易对它们周围的正常字段访问进行重新排序。写入 volatile 字段与监视器释放具有相同的记忆效应,从 volatile 字段读取与监视器获取具有相同的记忆效应。实际上,因为新的内存模型对易失性字段访问与其他字段访问(无论是否易失性)的重新排序施加了更严格的限制,线程 A 在写入易失性字段时可见的任何内容 f 对线程可见 B 当它读取 f 时。

JSR 133(Java 内存模型)常见问题解答

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

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

 // Declaration
public class SharedLocation {
    static public volatile SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using 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.
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 进行测试并发现它为真,然后被挂起。然后 thread-2 做同样的测试,也认为它是 true 并被挂起。然后线程 1 恢复并将计数器设置为 0。然后线程 2 恢复并再次将计数器设置为 0,因为它错过了线程 1 的更新。即使线程切换没有像我所描述的那样发生,这也可能发生,而仅仅是因为计数器的两个不同缓存副本存在于两个不同的 CPU 内核中,并且每个线程都在一个单独的内核上运行。就此而言,一个线程可能有一个值的计数器,而另一个线程可能有一些完全不同的值,只是因为缓存。

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

 MOV EAX,counter
INC EAX
MOV counter,EAX

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

原文由 Lawrence Dol 发布,翻译遵循 CC BY-SA 4.0 许可协议

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 在它的线程中,但是值在当前线程可能与更新后的值不同。事实上,Java 有“主”内存的概念,这是保存变量当前“正确”值的内存。线程可以拥有自己的变量数据副本,并且线程副本可以不同于“主”内存。所以实际上,对于 — ,“主”内存的值为 1 是可能的,对于 i1 i1 的值为 2 以及对于 thread2 具有值为 3 for i1 如果 thread1thread2 都更新了 i1 但这些更新的值尚未传播到“主”内存或其他线程。

另一方面, geti2() 有效地从“主”内存访问 i2 的值。 volatile 变量不允许拥有与当前保存在“主”内存中的值不同的变量的本地副本。实际上,声明为 volatile 的变量必须在所有线程中同步其数据,以便无论何时在任何线程中访问或更新该变量,所有其他线程都会立即看到相同的值。通常 volatile 变量比“普通”变量具有更高的访问和更新开销。通常允许线程拥有自己的数据副本以提高效率。

volitile 和 synchronized 之间有两个区别。

首先 synchronized 获取和释放监视器上的锁,这可以强制一次只有一个线程执行代码块。这是同步的众所周知的方面。但是 synchronized 也会同步内存。事实上,synchronized 将整个线程内存与“主”内存同步。所以执行 geti3() 执行以下操作:

  1. 线程获取对象 this 的监视器上的锁。
  2. 线程内存刷新它的所有变量,即它的所有变量都有效地从“主”内存中读取。
  3. 代码块被执行(在这种情况下将返回值设置为 i3 的当前值,它可能刚刚从“主”内存中重置)。
  4. (对变量的任何更改现在通常会写出到“主”内存,但对于 geti3() 我们没有任何更改。)
  5. 线程释放对象 this 的监视器上的锁。

所以其中volatile只同步线程内存和“主”内存之间的一个变量的值,synchronized同步线程内存和“主”内存之间的所有变量的值,并锁定和释放一个监视器以启动。显然 synchronized 可能比 volatile 有更多的开销。

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

原文由 Kerem Baydoğan 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题