一尘不染

Java中的volatile和Synchronized之间的区别

java

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


阅读 603

收藏
2020-02-28

共1个答案

一尘不染

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

  1. execution control, and
  2. memory visibility

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

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

使用volatile,而另一方面,将强制所有访问(读或写)到易失性可变发生到主存储器,有效地把挥发性变量out CPU的高速缓存。这对于某些仅要求变量的可见性正确且访问顺序不重要的操作很有用。使用volatile还改变了对它们的处理,long并double要求对其进行原子访问;在某些(较旧的)硬件上,这可能需要锁,但在现代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

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

2020-02-28