小编典典

Java 中 volatile 和 synchronized 的区别

all

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

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

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

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

读-更新-写 是什么意思?写入不是更新还是仅仅意味着 更新 是依赖于读取的写入?

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


阅读 68

收藏
2022-04-27

共1个答案

小编典典

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

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

第一个与控制代码何时执行(包括执行指令的顺序)以及它是否可以并发执行有关,第二个与其他线程何时可以看到已完成的内存中的效果有关。因为每个 CPU
在它和主内存之间都有几个级别的缓存,所以在不同的 CPU
或内核上运行的线程在任何给定的时间都可以看到不同的“内存”,因为线程被允许获取和工作在主内存的私有副本上。

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

volatile另一方面,使用 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
变量之间不能相互重新排序仍然是事实。不同之处在于,现在重新排序围绕它们的正常字段访问不再那么容易了。写入易失性字段与释放监视器具有相同的记忆效应,从易失性字段读取具有与监视器获取相同的记忆效应。实际上,由于新的内存模型对
volatile 字段访问与其他字段访问(无论是否为 volatile)的重新排序设置了更严格的限制,因此线程A在写入 volatile
字段时对线程可见的任何内容在读取时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
进行测试并发现它为真,然后被挂起。然后线程 2 进行相同的测试,并且也认为它是真的并被挂起。然后线程 1 恢复并将计数器设置为 0。然后线程 2
恢复并再次将计数器设置为 0,因为它错过了线程 1
的更新。即使没有如我所描述的那样发生线程切换,也可能发生这种情况,而仅仅是因为两个不同的计数器缓存副本存在于两个不同的 CPU
内核中,并且每个线程都在单独的内核上运行。就此而言,一个线程可能具有一个值的计数器,而另一个线程可能仅因为缓存而具有某个完全不同的值的计数器。

在这个例子中重要的是变量 计数器
从主存读取到缓存中,在缓存中更新,并且仅在稍后发生内存屏障或其他需要缓存内存时的某个不确定点写回主存。制作计数器volatile不足以保证这段代码的线程安全,因为对最大值的测试和分配是离散的操作,包括作为一组非原子read+increment+write机器指令的增量,例如:

MOV EAX,counter
INC EAX
MOV counter,EAX

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

2022-04-27