Java中volatile和synchronized之间的区别

时间:2010-08-19 07:34:45

标签: java multithreading java-me synchronized volatile

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

根据这篇文章http://www.javamex.com/tutorials/synchronization_volatile.shtml,有很多内容可以说,并且存在许多差异,但也存在一些相似之处。

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

  

...

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

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

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

6 个答案:

答案 0 :(得分:348)

答案 1 :(得分:93)

  

volatile 字段修饰符,而同步修改代码块方法。因此,我们可以使用这两个关键字指定简单访问器的三种变体:

    int i1;
    int geti1() {return i1;}

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

    int i3;
    synchronized int geti3() {return i3;}
     

geti1()访问当前线程中当前存储在i1中的值。   线程可以有变量的本地副本,并且数据不必与其他线程中保存的数据相同。特别是,另一个线程可能在其线程中更新了i1,但是当前线程中的值可能与更新的值不同。事实上,Java有一个“主”内存的概念,这是保存变量当前“正确”值的内存。线程可以拥有自己的变量数据副本,并且线程副本可以与“主”内存不同。事实上,对于i1,“主”内存可能具有 1 的值,因为thread1的值为 2 {如果 thread1 thread2 thread2 的值为 3 i1 >已更新i1但这些更新值尚未传播到“主”内存或其他线程。

     

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

     

volitile和synchronized之间有两个不同。

     

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

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

    因此,volatile只在线程内存和“主”内存之间同步一个变量的值,synchronized会同步线程内存和“主”内存之间所有变量的值,并锁定并释放一个监视器来启动。明确同步可能比volatile更具开销。

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

答案 2 :(得分:18)

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

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

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

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

enter image description here

要更好地了解volatile概念,请查看此article

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

关于您的其他查询

  

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

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

  

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

答案与第一次查询相同。

为了更好地理解,请参阅此article

答案 3 :(得分:2)

我喜欢jenkov's解释

共享对象的可见性

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

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

下图说明了草绘的情况。在左侧CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其 count 变量更改为2.此更改对于在正确的CPU上运行的其他线程不可见,因为要更新计数还没有被冲回主记忆。

enter image description here

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

竞争条件

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

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

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

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

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

enter image description here

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

答案 4 :(得分:1)

tl;博士

多线程存在三个主要问题:

1)比赛条件

2)缓存/过时的内存

3)编译器和CPU优​​化

volatile可以解决2和3,但不能解决1。synchronized /显式锁可以解决1、2和3。

详细说明

1)考虑此线程不安全代码:

x++;

虽然看起来像一个操作,但实际上是3:从内存中读取x的当前值,将x加1,然后将其保存回内存。如果很少有线程尝试同时执行此操作,则操作的结果不确定。如果x最初是1,则在2个线程对代码进行操作之后,它可能是2,也可能是3,这取决于在控制权转移到另一个线程之前哪个线程完成了操作的哪一部分。这是比赛条件的一种形式。

在代码块上使用synchronized使其具有 atomic 的功能-这意味着它使3个操作一次完成,而中间没有其他线程并干涉。因此,如果x为1,并且有2个线程尝试执行x++,则我们知道最终等于3。因此,它解决了竞争条件问题。

synchronized (this) {
   x++; // no problem now
}

x标记为volatile不会使x++;原子化,因此不能解决此问题。

2)此外,线程具有自己的上下文-即它们可以从主内存中缓存值。这意味着一些线程可以具有变量的副本,但是它们在其工作副本上进行操作,而不会在其他线程之间共享变量的新状态。

考虑在一个线程上x = 10;。稍后,在另一个线程中,x = 20;x的值更改可能不会出现在第一个线程中,因为另一个线程已将新值保存到其工作内存中,但尚未将其复制到主内存中。或者它确实将其复制到主内存,但第一个线程尚未更新其工作副本。因此,如果现在第一个线程检查if (x == 20),答案将是false

将变量标记为volatile基本上告诉所有线程仅在主内存上执行读写操作。 synchronized告诉每个线程进入块时都要从主内存中更新其值,并在退出块时将结果刷新回主内存中。

请注意,与数据争用不同,过时的内存并不是那么容易(重新)生成,因为无论如何都会刷新到主内存。

3)编译器和CPU可以(没有线程之间的任何形式的同步)将所有代码视为单线程。这意味着它可以查看一些在多线程方面非常有意义的代码,并将其视为单线程,而不是那么有意义。因此,如果不知道这段代码是设计用于多个线程的,它可以查看代码并出于优化考虑而决定对其重新排序,甚至完全删除其中的一部分。

考虑以下代码:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

您会认为线程B只能打印20(如果在将b设置为true之前执行threadB if-check,则线程B不能打印任何内容),因为b仅在{之后才设置为true {1}设置为20,但是编译器/ CPU可能决定对线程A重新排序,在这种情况下,线程B也可以打印10。将x标记为b可确保不会对其重新排序(或在某些情况下被丢弃)。这意味着threadB只能打印20(或什么也不打印)。将方法标记为已同步将获得相同的结果。另外,将变量标记为volatile只能确保不会对其进行重新排序,但是仍可以对它之前/之后的所有内容进行重新排序,因此在某些情况下同步可能更适合。

请注意,在Java 5 New Memory Model之前,volatile无法解决此问题。

答案 5 :(得分:0)

Java中的

volatile [About]synchronized [About]关键字是为在多线程环境中工作而创建的。他们在一起创造了很好的串联解决方案,解决了很大一部分问题

让我们看一个非常简单的示例-我们应该在多线程环境中增加一个变量

问题1-共享资源-多个线程可以使用可以保存在CPU寄存器或CPU缓存中的数据的本地副本
解决方案1-volatile-从RAM中写入和读取值-主存储器

问题2-竞争条件-多个线程可以同时更新/写入一个值,结果只有一个线程的操作会产生影响
解决方案2-synchronized-监视器只能在一个线程中被阻塞,而其他线程则在释放监视器时等待

相关主题为Compare and swap [About]