286

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

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

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

...

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

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

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

4

4 回答 4

440

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

  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对线程可见。Bf

-- 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

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

于 2010-08-19T07:48:33.657 回答
110

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

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

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

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

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

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

易变和同步之间有两个区别。

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

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

所以其中 volatile 只在线程内存和“主”内存之间同步一个变量的值,synchronized 在线程内存和“主”内存之间同步所有变量的值,并锁定和释放一个监视器以启动。显然,同步的开销可能比 volatile 的开销更大。

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

于 2010-08-19T07:50:55.973 回答
47

tl;博士

多线程有3个主要问题:

1) 比赛条件

2)缓存/陈旧的内存

3) 编译器和 CPU 优化

volatile可以解决 2 & 3,但不能解决 1。synchronized/显式锁可以解决 1、2 和 3。

阐述

1)考虑这个线程不安全的代码:

x++;

虽然看起来像是一个操作,但实际上是 3:从内存中读取 x 的当前值,将其加 1,然后将其保存回内存。如果少数线程同时尝试执行此操作,则操作的结果是未定义的。如果x最初是1,那么在2个线程操作代码之后可能是2,也可能是3,这取决于哪个线程完成了控制权转移到另一个线程之前的哪部分操作。这是一种竞态条件

在代码块上使用synchronized使其具有原子性——这意味着它使 3 个操作好像同时发生,并且另一个线程无法进入中间并进行干扰。所以如果x是 1,并且有 2 个线程尝试执行x++我们知道最终它将等于 3。所以它解决了竞争条件问题。

synchronized (this) {
   x++; // no problem now
}

标记xvolatile不会使原子成为x++;原子,所以它不能解决这个问题。

2) 此外,线程有自己的上下文——即它们可以缓存主内存中的值。这意味着少数线程可以拥有一个变量的副本,但它们在其工作副本上进行操作,而不在其他线程之间共享该变量的新状态。

考虑在一个线程上,x = 10;. 稍后,在另一个线程中,x = 20;. 值的变化x可能不会出现在第一个线程中,因为另一个线程已将新值保存到其工作内存中,但尚未将其复制到主内存中。或者它确实将其复制到主内存,但第一个线程尚未更新其工作副本。因此,如果现在第一个线程检查if (x == 20)答案将是false.

将变量标记为volatile基本上告诉所有线程仅在主内存上执行读写操作。synchronized告诉每个线程在进入块时从主存更新它们的值,并在它们退出块时将结果刷新回主存。

请注意,与数据竞争不同,陈旧的内存并不那么容易(重新)生产,因为无论如何都会刷新到主内存。

3) 编译器和 CPU 可以(在线程之间没有任何形式的同步)将所有代码视为单线程。这意味着它可以查看一些在多线程方面非常有意义的代码,并将其视为单线程,而这并不是那么有意义。因此,它可以查看代码并决定,为了优化,重新排序它,甚至完全删除它的一部分,如果它不知道这段代码是为在多个线程上工作而设计的。

考虑以下代码:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

您会认为 threadB 只能打印 20(或者如果在设置b为 true 之前执行 threadB if-check,则根本不打印任何内容),因为b只有在x设置为 20 之后才设置为 true,但编译器/CPU 可能会决定重新排序threadA,在这种情况下,threadB 也可以打印 10。标记bvolatile确保它不会被重新排序(或在某些情况下被丢弃)。这意味着 threadB 只能打印 20 (或根本不打印)。将方法标记为同步将获得相同的结果。还将变量标记为volatile仅确保它不会被重新排序,但它之前/之后的所有内容仍然可以重新排序,因此在某些情况下同步可能更适合。

请注意,在 Java 5 新内存模型之前,volatile 并没有解决这个问题。

于 2019-02-08T12:43:24.857 回答
24

synchronized是方法级/块级访问限制修饰符。它将确保一个线程拥有临界区的锁。只有拥有锁的线程才能进入synchronized阻塞。如果其他线程试图访问这个临界区,它们必须等到当前所有者释放锁。

volatile是变量访问修饰符,它强制所有线程从主内存中获取变量的最新值。volatile访问变量不需要锁定。所有线程都可以同时访问 volatile 变量值。

使用 volatile 变量的一个很好的例子:Date变量。

假设您已将 Date 设为变量volatile。访问此变量的所有线程始终从主内存获取最新数据,以便所有线程显示真实(实际)日期值。您不需要不同的线程为同一变量显示不同的时间。所有线程都应显示正确的日期值。

在此处输入图像描述

查看这篇文章以更好地理解volatile概念。

Lawrence Dol cleary 解释了你的read-write-update query.

关于您的其他查询

什么时候声明变量 volatile 比通过同步访问它们更合适?

volatile如果您认为所有线程都应该实时获取变量的实际值,则必须使用,就像我为 Date 变量解释的示例一样。

对依赖于输入的变量使用 volatile 是个好主意吗?

答案将与第一个查询相同。

请参阅这篇文章以获得更好的理解。

于 2015-12-18T12:43:08.787 回答