繁体   English   中英

Java中volatile和synchronized的区别

[英]Difference between volatile and synchronized in Java

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

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

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

...

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

他们所说的read-update-write是什么意思? 不是写也是更新还是他们只是意味着更新是依赖于读的写?

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

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

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

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

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

另一方面,使用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 内核中,并且每个线程都在一个单独的内核上运行。 就此而言,一个线程可能有一个值的计数器,而另一个线程可能会因为缓存而有一些完全不同的值。

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

MOV EAX,counter
INC EAX
MOV counter,EAX

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

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的值i1 ,对线程1为具有2的值, i1线程2具有用于一个3i1如果线程1线程都既更新i1 但那些更新的值尚未传播到“主”内存或其他线程。

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

volitile 和 synchronized 之间有两个区别。

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

tl;博士

多线程有3个主要问题:

1) 竞争条件

2)缓存/陈旧内存

3) 编译器和 CPU 优化

volatile可以解决2&3,但是不能解决synchronized /explicit locks可以解决1,2&3。

阐述

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

x++;

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

在代码块上使用synchronized使其具有原子性- 这意味着它就像 3 个操作synchronized一样,并且没有办法让另一个线程进入中间并进行干扰。 因此,如果x为 1,并且 2 个线程尝试执行x++我们知道最终它会等于 3。因此它解决了竞争条件问题。

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

x标记为volatile不会使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。将b标记为volatile确保它不会被重新排序(或在某些情况下被丢弃)。 这意味着 threadB 只能打印 20 个(或根本不打印)。 将方法标记为同步将获得相同的结果。 将变量标记为volatile只能确保它不会被重新排序,但它之前/之后的所有内容仍然可以重新排序,因此同步在某些情况下更适合。

请注意,在 Java 5 New Memory Model 之前, volatile 并没有解决这个问题。

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

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

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

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

在此处输入图片说明

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

Lawrence Dol cleary 解释了您read-write-update query

关于您的其他查询

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

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

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

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

请参阅本文以获得更好的理解。

我喜欢詹科夫的解释。 多线程环境。

共享对象的可见性

如果两个或多个线程共享一个对象,而没有适当地使用volatile声明或同步 ,则一个线程对共享对象进行的更新可能对其他线程不可见。

想象一下,共享对象最初存储在主存储器中。 然后,在CPU一个上运行的线程将共享对象读入其CPU缓存中。 在那里,它更改了共享库。 只要未将CPU高速缓存刷新回主内存,就可以在其他CPU上运行的线程看不到共享对象的更改版本。 这样,每个线程都可以拥有自己的共享库副本,每个副本位于不同的CPU缓存中。

下图说明了这种情况。 一个线程左侧CPU运行副本共享对象到其CPU高速缓存,并改变计数变量为2。这改变是正确的CPU上运行的其他线程不可见的,因为算上更新还没有被冲回主记忆呢。

易挥发[关于]

要解决此问题,可以使用Java的volatile关键字 volatile keyword可以确保:

  1. 给定变量直接从主存储器 读取 ,并且在更新时始终回到主存储器
  2. 发生之前。

比赛条件

如果两个或多个线程共享一个对象,并且有多个线程更新该共享对象中的变量,则可能会发生race conditions

想象一下, thread A将共享库的变量计数读入其CPU缓存中。 还要想象一下, thread B功能相同,但是它位于不同的CPU缓存中。 现在thread A一个要计数, thread B执行相同的操作。 现在var1已经增加了两次 ,在每个CPU缓存中增加一次。

如果这些增加是顺序执行的 ,则变量计数将增加两次,并将原始值+ 2写回到主存储器。

但是,这两个增量是在没有适当同步的情况下同时执行的。 不管线程A和B中哪个线程将其更新后的计数写回主存,尽管有两个增量,更新后的值仅比原始值高1。

该图说明了如上所述的竞争条件问题的发生:

已同步[关于]

要解决此问题,您可以使用synchronized

  1. 同步块可确保在任何给定时间只有一个线程可以输入代码的给定关键部分。
  2. 发生之前。

相关主题比较和交换

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM