挥发性和Java中同步的区别

问题描述 投票:200回答:5

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

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

我在这一块信息特别感兴趣:

...

  • 访问volatile变量从来没有阻止的潜力:我们永远只能做一个简单的读或写,一点都不像一个synchronized块,我们将永远坚持到任何锁;
  • 因为访问volatile变量从未持有锁,它是不适合的情况下,我们想读取,更新,写作为一个原子操作(除非我们准备“会错过的更新”);

他们是什么意思通过读取,更新,写?不是写也是一个更新,还是他们只是意味着更新是取决于读写?

最重要的是,当是更适合来声明变量volatile而不是通过synchronized块访问它们?它是使用volatile对于那些依赖于输入变量是一个好主意?例如,有一个叫做render变量,通过渲染循环读取和设置的按键事件?

java multithreading java-me synchronized volatile
5个回答
358
投票

要明白,有两个方面的线程安全是很重要的。

  1. 执行控制,并
  2. 内存知名度

第一个有控制时做代码执行(包括其中的指令执行顺序),以及它是否能够并发执行的,第二个做的时候在做了什么记忆的效果对其它线程可见用。因为每个CPU有它和主内存,在不同的CPU或核上运行的线程之间的高速缓存的几个层次能及时在任何特定时刻看到“记忆”不同,因为线程被允许获得和主内存的传抄工作。

使用synchronized防止任何其他线程从获得的监视器(或锁定)对于同一对象,从而防止由同步对同一对象从同时执行保护的所有代码块。同步还创建了一个“之前发生”记忆障碍,引起内存可见性约束,使得任何事情到一些点线程释放出现锁定另一个线程随后获取同一个锁它获得锁之前已经发生。在实践中,对当前的硬件,这通常将导致CPU的高速缓存的冲洗时的监视器被获取并当它被释放写入主存储器,这两者都是(相对)昂贵的。

使用volatile,在另一方面,迫使所有的访问(读或写)到易失性可变发生到主存储器,有效地把挥发性变量out CPU的高速缓存。它仅仅是需要变量的可视性是正确的,访问的顺序并不重要这对于一些操作时非常有用。使用volatile也改变治疗longdouble要求访问到它们是原子的;在一些(旧的)硬件,这可能需要锁的,虽然不是现代的64位硬件。根据新的(JSR-133),内存为Java 5+模式,挥发性语义已经加强,因为相对于内存的知名度和指令排序(见http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile)同步是几乎一样强烈。能见度的目的,每个访问的易失性字段的作用就像半个同步。

在新的内存模型,它仍然是事实,volatile变量不能相互进行重新排序。所不同的是,它现在不再那么容易重新安排正常的字段访问他们周围。写入到易失性字段具有相同的记忆效应作为监视器释放,以及从易失性字段读取具有相同的记忆效应作为监视器获取。实际上,因为新的内存模型放在挥发性场的重新排序严格限制访问与其他字段访问,挥发性与否,任何东西,这是明显的线程A时将其写入volatile字段f变得可见线程B时,它读取f

- JSR 133 (Java Memory Model) FAQ

所以,现在存储器屏障的两种形式(当前JMM下)造成指令重新排序屏障防止编译器或运行时从整个屏障重新排序的指令。在旧的JMM,挥发并没有阻止重新排序。这可能是重要的,因为除了内存屏障施加的唯一限制是,对于任何特定线程代码的实际效果是一样的,如果说明是在精确它们出现的顺序执行这将是资源。

一个使用的易失性为共享,但不可变的对象被重建上的苍蝇,与许多其他的线程在其执行周期中的特定点拍摄的对象的引用。一个人需要其他线程开始使用它一旦公布的重建对象,但并不需要完全同步的额外开销和它的随之而来的竞争和缓存刷新。

// 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确实为反== 1000测试并发现它真正的,然后暂停。然后线程2做了同样的试验,也看到它真实和暂停。然后线程1简历并且将计数器为0。然后线程2简历并且再次计数器设置为0,因为它错过从线程1中的更新。这也可能发生,即使如我所描述的,不会发生线程切换,而计数器的仅仅是因为两个不同的缓存副本存在于两个不同的CPU内核和每一个运行在一个单独的内核线程。对于这个问题,一个线程可以在一个值有计数器和其他可在一些完全不同的价值,只是因为缓存具有抗衡。

是什么在这个例子中重要的是,可变计数器从主存储器到高速缓存中读取,更新缓存和只写回主内存的一些不确定点之后出现了记忆障碍或需要别的东西高速缓存存储器时的时候。使计数器volatile不足以这段代码的线程安全的,因为测试的最大值和分配是不连续的操作,包括这是一组非原子read+increment+write机器指令,喜欢的东西增量:

MOV EAX,counter
INC EAX
MOV counter,EAX

volatile变量是有用的,只有当它们执行的所有操作都是“原子”,比如我的例子,其中一个完全成型的对象的引用只能读取或写入(事实上,它通常会只从单点写的)。另一个例子是易失性阵列基准背衬一个写入时复制列表中,所提供的阵列仅由第一取参考的本地副本来读取。


94
投票

挥发性是场改性剂,而同步的修改的代码块和方法。因此,我们可以指定使用这两个关键字,一个简单的访问的三个变化:

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

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

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

geti1()访问当前存储在i1在当前线程的值。线程可以具有的变量拷贝,数据不一定是相同的其他threads.In特别保存的数据,另一个线程可能已更新i1在它的线程,但在当前线程的值可以是来自不同其更新值。事实上,Java有一个“主”内存的想法,这是存放了变量目前的“准确”值的内存。线程可以有自己的变量数据的拷贝,以及线程副本可从“主”内存不同。所以,事实上,可能的是“主”存储器以具有1 i1的值,线程1为具有2 i1一个值和线程2为具有值3为i1如果线程1和线程都既更新I1但这些更新的价值尚未被传播到“主”内存或其他线程。

在另一方面,有效geti2()访问i2的从“主”存储器中的值。 volatile变量是不允许有一个变量是目前在“主”内存的值不同的本地副本。实际上,一个变量声明为volatile所有线程必须同步它的数据,所以,每当你访问或更新任何线程变量,所有其他线程立即看到相同的值。一般volatile变量比“普通”变量更高的访问和更新的开销。一般来说线程被允许有自己的数据副本是为了提高效率。

有挥发性和同步的两个差异。

首先同步在监视器上,其可以迫使仅一个线程的时间来执行码块获得并释放锁。那是相当著名的方面同步。但也同步同步内存。事实上,synchronized同步线程内存有“主”内存整体。因此,在执行geti3()执行以下操作:

  1. 该线程获得监视器对象本上的锁。
  2. 线程内存刷新其所有的变量,即已将其所有变量有效地从“主”内存读取。
  3. 代码块被执行(在这种情况下设定的返回值I3的电流值,这可能刚刚从“主”存储器复位)。
  4. (变量的任何更改通常会现在写到“主”内存,但对于geti3()方法,我们有没有变化。)
  5. 该线程释放监视器对象本上的锁。

那么,挥发性仅同步线程内存和“主”内存之间的一个变量的值,同步同步线程内存和“主”内存,并锁定和释放监视器启动之间的所有变量的值。显然synchronized要比volatile更多的开销。

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


19
投票

synchronized方法是水平/块级访问限制修饰符。这将确保一个线程拥有的临界区的锁。只有线程,拥有锁可以进入synchronized块。如果其他线程试图访问这个关键部分,他们必须等到当前所有者释放锁。

volatile是可变的访问修饰符,这迫使所有线程获取从主内存中变量的最新值。无锁定才能访问volatile变量。所有的线程可以同时访问volatile变量值。

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

假设你已经取得日期变量volatile。所有线程,其访问这个变量总是从主内存中的最新数据,以便所有线程显示真正的(实际)日期值。你并不需要不同的线程显示了同一个变量不同的时间。所有线程都应该表现出正确的日期值。

enter image description here

看看这个article更好地理解volatile概念。

劳伦斯·多尔克利解释你的read-write-update query

关于你提到的其他查询

如果是更适合来声明变量波动不是通过同步访问它们?

你有,如果你认为所有的线程应该得到像我为日期变量解释的例子实时变量的实际值使用volatile

它是使用volatile对于那些依赖于输入变量是一个好主意?

答案是一样的第一个查询。

请参阅本article为了更好的理解。


3
投票

TL;博士:

有与多线程3个主要问题:

1)竞争条件

2)缓存/存储器陈旧

3)编译器和CPU的优化

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

阐述:

1)考虑这个线程不安全的代码:

x++;

虽然它可能看起来像一个操作,它实际上是3:从存储器读取x的当前值,加1到它,并将其保存回内存。如果几个线程试图在同一时间做,操作的结果是不确定的。如果x原本为1,后2个线程操作的代码可以是2,它可以是3,这取决于哪个线程完成控制之前的操作的哪一部分被转移到其它线程。这是一个竞争条件的形式。

一个代码块上使用synchronized使得原子 - 这意味着它使它仿佛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);
    }
}

你可能会认为threadB只能打印20(或不打印任何东西,如果threadB如果检查执行之前b设置为true),因为b设置为true x为20只后,但是编译器/ CPU可能决定重新排序的ThreadA,在这种情况下也threadB可以打印10标记b作为volatile确保它不会被重新排序(或在某些情况下被丢弃)。这意味着threadB只能打印20(或什么都没有)。这标志着作为方法将syncrhonized达到同样的效果。也标志着一个变量volatile只有确保它不会重新排序,但一切前/后仍然可以重新排序,所以同步功能,可以更适合在某些情况下。

需要注意的是Java 5的新的内存模型之前,挥发性没有解决这个问题。


2
投票

我喜欢jenkov's解释。多线程envirompment。

共享对象的可见性

如果两个或多个线程共享的对象,如果没有适当的使用是易失性声明或同步的,更新由一个线程进行的共享对象可以不给其他线程是可见的。

想象,共享对象被初始存储在主存储器中。 CPU上运行的一个线程然后读取共享对象到其CPU高速缓存。在那里,它进行了更改共享对象。只要CPU缓存没有被冲回主内存,改变的版本的共享对象是不是在其他CPU上运行的线程是可见的。这样,每个线程可以有自己的共享对象的副本结束了,每个副本都坐在不同的CPU缓存。

下图说明了绘制的情况。一个线程左侧CPU运行副本共享对象到其CPU高速缓存,并改变其计数变量为2。这改变是正确的CPU上运行的其他线程不可见的,因为算上更新还没有被冲回主记忆呢。

为了解决这个问题,你可以使用Java's volatile keyword。该volatile keyword[About]可以确保一个给定的变量从主内存中直接读取和更新的时候总是写回主内存。

竞态条件

如果两个或多个线程共享的对象,并且在该共享对象多于一个的线程更新变量,可能会发生race conditions

想像一下,如果thread A读取的共享对象的变量计数到它的CPU高速缓存。试想过,那thread B不相同,但为不同的CPU缓存。现在thread A加一算,和thread B不相同。现在VAR1已递增两次,一次在每个CPU缓存。

如果这些增量已被顺序地进行时,可变计数将被递增两次并具有原始值+ 2写回到主存储器中。

然而,这两个增量已同时进行,如果没有适当的同步。不管哪个线程A和B写入计数及其更新版本回主内存的,更新的价值只会比原来的值增加1,尽管两个增量。

此图说明如以上竞态条件的问题的发生:

为了解决这个问题,你可以使用一个Java synchronized block

  1. 同步的块保证了只有一个线程可以在任何给定时间进入码的一个给定的临界区。
  2. synchronized块也保证同步块内访问的所有变量都从主内存中读取,而当线程退出synchronized块时,所有更新的变量将被再次刷新回主存储器,不管变量是否被声明volatile或不。

相关主题Compare and swap

© www.soinside.com 2019 - 2024. All rights reserved.