java - volatile详解 - volatile使用场景



Java中volatile和synchronized之间的区别 (3)

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

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

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

...

  • 对一个volatile变量的访问永远不会有阻塞的可能:我们只做过简单的读或写操作,所以与synchronized块不同,我们永远不会锁住任何锁;
  • 因为访问一个volatile变量永远不会拥有一个锁,所以它不适用于我们想要读 - 更新 - 写为原子操作的情况(除非我们准备“错过更新”);

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

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


Answer #1

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 ,“main”内存可能有1的值,如果thread1thread2都更新了,则thread1的 i1的值为2 ,而i1thread2的值为3 i1但这些更新的值尚未传播到“主”内存或其他线程。

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

volitile和synchronized有两个区别。

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

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

因此,只有volatile才会同步线程内存和“主”内存之间的一个变量的值,synchronized会同步线程内存和“主”内存之间的所有变量的值,并锁定和释放显示器以启动。 明显同步可能会比易失性更多的开销。

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


Answer #2
  1. java中的volatile关键字是一个字段修饰符, synchronized修改代码块和方法。

  2. synchronized获取并释放监视器的java volatile关键字不需要这样做。

  3. Java中的线程可以被阻塞,以便在synchronized情况下等待任何监视器,而Java中的volatile关键字则不是这种情况。

  4. 在Java中, synchronized方法比volatile关键字更影响性能。

  5. 由于Java中的volatile关键字只同步线程内存和“主”内存之间的一个变量的值,而synchronized关键字同步线程内存和“主”内存之间的所有变量的值,并锁定和释放要启动的显示器。 由于这个原因,Java中的synchronized关键字可能比volatile更具开销。

  6. 你不能在空对象上同步,但你的Java中的volatile变量可能为空。

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


Answer #3

理解线程安全有两个方面很重要:(1)执行控制;(2)内存可视性。 第一个是控制代码执行的时间(包括执行指令的顺序)以及它是否可以并发执行,第二个是当内存中的效果对其他线程可见时执行。 由于每个CPU在其与主内存之间都有多个级别的缓存,因此在任何特定时刻运行在不同CPU或内核上的线程都可以看到“内存”,因为允许线程获取并在主内存的专用副本上工作。

使用synchronized可以防止任何其他线程获取同一对象的监视器(或锁定),从而防止同一对象上同步保护的所有代码块不会同时执行。 同步还会创建一个“发生在之前”的内存障碍,导致内存可见性约束,以致某些线程释放锁的任何事情都会出现在另一个线程上,随后获取之前发生的同一个锁。 实际上,在当前的硬件上,这通常会在获取监视器时导致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 )。 出于可见性的目的,对易失性字段的每次访问都像半个同步一样。

在新的内存模型下,volatile变量不能彼此重新排序仍然是事实。 不同之处在于现在不再那么容易对他们周围的普通字段进行重新排序。 写入易失性字段与监视器版本具有相同的记忆效应,并且从易失性字段读取具有与监视器获取相同的记忆效应。 实际上,由于新内存模型对易失性字段访问进行其他字段访问的重新排序有更严格的限制,不管是否为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进行测试,并发现它为真,然后暂停。 然后,线程2执行相同的测试,并且看到它是真实的并且被暂停。 然后,线程1恢复并将计数器设置为0.然后,线程2恢复并再次将计数器设置为0,因为它错过了线程1的更新。 这也可能发生,即使线程切换没有像我所描述的那样发生,但仅仅是因为在两个不同的CPU内核中存在两个不同的缓存副本,并且每个线程都在单独的内核上运行。 对于这个问题,一个线程可能只有一个值,另一个线程可能因为缓存而具有完全不同的值。

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

MOV EAX,counter
INC EAX
MOV counter,EAX

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





volatile