Java中易失性和同步之间的区别

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

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

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

...

  • 访问易失性变量永远不会有阻塞的潜力:我们只做一个简单的读取或写入,所以与同步块不同,我们永远不会持有任何锁;
  • 因为访问易失性变量永远不会有锁,所以它不适合我们想要读-更新-写作为原子操作的情况(除非我们准备“错过更新”);

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

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


答案 1

重要的是要了解线程安全有个方面。

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

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

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

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

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

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

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

此示例中重要的是,变量计数器从主内存读取到缓存中,在缓存中更新,并且仅在以后发生内存障碍或需要缓存内存时在某个不确定的时间点写回主内存。制作计数器对于此代码的线程安全性是不够的,因为对最大值和赋值的测试是离散操作,包括增量,它是一组非原子机器指令,如下所示:volatileread+increment+write

MOV EAX,counter
INC EAX
MOV counter,EAX

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


答案 2

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

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

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

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

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

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

volitile和sync之间有两个区别。

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

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

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

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