Java中的volatile和Synchronized之间的区别

阿不思·邓布利多:

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

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

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

...

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

读-更新-写是什么意思写不仅是更新,还是仅表示更新是取决于读取的写?

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

劳伦斯·多尔(Lawrence Dol):

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

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

第一个与控制代码何时执行(包括执行指令的顺序)以及是否可以并发执行有关,第二个与何时在存储器中看到的已完成效果对其他线程可见有关。由于每个CPU与主内存之间都具有多个高速缓存级别,因此运行在不同CPU或内核上的线程在任何给定的时间可以看到不同的“内存”,因为允许线程获取并使用主内存的专用副本。

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

使用volatile,而另一方面,将强制所有访问(读或写)到易失性可变发生到主存储器,有效地把挥发性变量out CPU的高速缓存。对于某些仅要求变量的可见性正确且访问顺序不重要的操作,这可能会很有用。使用volatile还改变了对它们的处理,longdouble要求对其进行原子访问;在某些(较旧的)硬件上,这可能需要锁,但在现代64位硬件上则不需要。在适用于Java 5+的新(JSR-133)内存模型下,就内存可见性和指令顺序而言,volatile的语义已得到增强,几乎与同步一样强大(请参阅http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile)。出于可见性的目的,对易失字段的每次访问都像同步的一半。

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

- 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()方法的情况下,两个线程可以同时输入它。在可能发生的许多变化中,一个是线程1对counter == 1000进行了测试,发现它为true,然后被挂起。然后线程2进行了相同的测试,并且也看到它是正确的并被挂起。然后线程1恢复并将计数器设置为0。然后线程2恢复并再次将计数器设置为0,因为它错过了线程1的更新。即使未发生线程切换,也可能发生这种情况,这仅仅是因为两个不同的CPU内核中存在两个不同的缓存计数器计数器副本,并且每个线程都在一个单独的内核上运行。为此,一个线程可能由于缓存而在一个值上具有计数器,而另一个线程可能在某个完全不同的值上具有计数器。

在此示例中重要的是,将变量计数器从主存储器读取到高速缓存中,然后在高速缓存中进行更新,并且仅在发生内存屏障或其他情况下需要高速缓存时,才在某个不确定的时间点将其写回主存储器。volatile对于该代码的线程安全而言,使计数器不足是因为对最大值的测试和分配是离散操作,包括增量(一组非原子read+increment+write机器指令),例如:

MOV EAX,counter
INC EAX
MOV counter,EAX

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

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章