我想知道在Java中将变量声明为易失性
和始终访问synchronized(this)
块中的变量之间有什么区别?
根据这篇文章 http://www.javamex.com/tutorials/synchronization_volatile.shtml有很多话要说,有很多不同之处,但也有一些相似之处。
我对这条信息特别感兴趣:
...
- 对易失性变量的访问永远不会被阻塞:我们只进行简单的读取或写入,因此与同步块不同,我们永远不会持有任何锁;
- 因为访问易失性变量永远不会持有锁,所以它不适合我们想要读取-更新-写入作为原子操作的情况(除非我们准备“错过更新”) ”);
读取-更新-写入是什么意思?写入不是也是更新吗?或者它们只是意味着更新是依赖于读取的写入?
最重要的是,什么时候声明变量volatile
比通过synchronized
块访问它们更合适?对于依赖于输入的变量使用 volatile 是一个好主意吗?例如,有一个名为 render
的变量,它是通过渲染循环读取并由按键事件设置的?
I am wondering at the difference between declaring a variable as volatile
and always accessing the variable in a synchronized(this)
block in Java?
According to this article http://www.javamex.com/tutorials/synchronization_volatile.shtml there is a lot to be said and there are many differences but also some similarities.
I am particularly interested in this piece of info:
...
- access to a volatile variable never has the potential to block: we're only ever doing a simple read or write, so unlike a synchronized block we will never hold on to any lock;
- because accessing a volatile variable never holds a lock, it is not suitable for cases where we want to read-update-write as an atomic operation (unless we're prepared to "miss an update");
What do they mean by read-update-write? Isn't a write also an update or do they simply mean that the update is a write that depends on the read?
Most of all, when is it more suitable to declare variables volatile
rather than access them through a synchronized
block? Is it a good idea to use volatile
for variables that depend on input? For instance, there is a variable called render
that is read through the rendering loop and set by a keypress event?
发布评论
评论(4)
重要的是要了解线程安全有两个方面。
第一个与控制代码何时执行(包括执行指令的顺序)以及是否可以并发执行有关,第二个与已完成的操作对内存的影响何时发生有关对其他线程可见。由于每个 CPU 和主内存之间都有多个级别的高速缓存,因此运行在不同 CPU 或内核上的线程在任何给定时刻都可以以不同的方式查看“内存”,因为允许线程获取主内存的私有副本并在其上工作。
使用
synchronized
可防止任何其他线程获取同一对象的监视器(或锁),从而防止所有受同步保护的代码块 >在同一个对象上并发执行。同步还创建一个“发生在”内存屏障,从而导致内存可见性约束,使得在某个线程释放锁之前所做的任何事情出现给另一个随后获取锁的线程在获取锁之前发生了相同的锁定。实际上,在当前的硬件上,这通常会导致在获取监视器时刷新 CPU 缓存,并在释放监视器时写入主内存,这两者(相对)昂贵。另一方面,使用 易失性 会强制对易失性变量的所有访问(读或写)发生在主内存中,从而有效地将易失性变量排除在 CPU 缓存之外。这对于某些仅要求变量可见性正确并且访问顺序并不重要的操作非常有用。使用
易失性
还改变了long
和double
的处理,要求对它们的访问是原子的;在某些(较旧的)硬件上,这可能需要锁,但在现代 64 位硬件上则不需要。在 Java 5+ 的新 (JSR-133) 内存模型下,易失性的语义已得到加强,在内存可见性和指令排序方面几乎与同步一样强大(请参阅 http://www.cs.umd.edu/users/pugh/java/memoryModel /jsr-133-faq.html#易失性)。出于可见性的目的,对易失性字段的每次访问都相当于半次同步。因此,现在两种形式的内存屏障(在当前的 JMM 下)都会导致指令重新排序屏障,从而阻止编译器或运行时跨屏障重新排序指令。在旧的 JMM 中,易失性并不能阻止重新排序。这可能很重要,因为除了内存屏障之外,唯一的限制是,对于任何特定线程,代码的净效果与指令精确地执行时的效果相同。它们在源中出现的顺序。
易失性的一种用途是用于动态重新创建共享但不可变的对象,许多其他线程在其执行周期的特定点引用该对象。一旦发布了重新创建的对象,就需要其他线程开始使用该对象,但不需要完全同步的额外开销以及随之而来的争用和缓存刷新。
具体来说,就是谈谈你的读-更新-写问题。考虑以下不安全代码:
现在,由于 updateCounter() 方法不同步,两个线程可能会同时进入该方法。在可能发生的情况有多种排列中,其中之一是线程 1 测试 counter==1000 并发现它为真,然后被挂起。然后线程 2 执行相同的测试,也看到它为真并被挂起。然后线程 1 恢复并将计数器设置为 0。然后线程 2 恢复并再次将计数器设置为 0,因为它错过了线程 1 的更新。即使线程切换没有像我所描述的那样发生,这种情况也可能发生,但这仅仅是因为两个不同的 CPU 内核中存在两个不同的计数器缓存副本,并且每个线程都在单独的内核上运行。就这一点而言,一个线程可以将计数器设置为一个值,而另一个线程可以将计数器设置为某个完全不同的值,只是因为缓存。
在这个例子中重要的是,变量counter从主内存读入缓存,在缓存中更新,并且仅在内存屏障发生或缓存内存发生时的某个不确定点写回主内存。需要做其他事情。使计数器
易失性
不足以保证此代码的线程安全,因为最大值的测试和赋值是离散操作,包括增量,它是一组非原子read+增量+写入
机器指令,类似于:仅当对它们执行的所有操作都是“原子”时,易失性变量才有用,例如我的示例,其中引用一个完全形成的对象只能被读取或写入(事实上,通常它只是从一个点写入)。另一个示例是支持写时复制列表的易失性数组引用,前提是仅通过首先获取对其引用的本地副本来读取该数组。
It's important to understand that there are two aspects to thread safety.
The first has to do with controlling when code executes (including the order in which instructions are executed) and whether it can execute concurrently, and the second to do with when the effects in memory of what has been done are visible to other threads. Because each CPU has several levels of cache between it and main memory, threads running on different CPUs or cores can see "memory" differently at any given moment in time because threads are permitted to obtain and work on private copies of main memory.
Using
synchronized
prevents any other thread from obtaining the monitor (or lock) for the same object, thereby preventing all code blocks protected by synchronization on the same object from executing concurrently. Synchronization also creates a "happens-before" memory barrier, causing a memory visibility constraint such that anything done up to the point some thread releases a lock appears to another thread subsequently acquiring the same lock to have happened before it acquired the lock. In practical terms, on current hardware, this typically causes flushing of the CPU caches when a monitor is acquired and writes to main memory when it is released, both of which are (relatively) expensive.Using
volatile
, on the other hand, forces all accesses (read or write) to the volatile variable to occur to main memory, effectively keeping the volatile variable out of CPU caches. This can be useful for some actions where it is simply required that visibility of the variable be correct and order of accesses is not important. Usingvolatile
also changes treatment oflong
anddouble
to require accesses to them to be atomic; on some (older) hardware this might require locks, though not on modern 64 bit hardware. Under the new (JSR-133) memory model for Java 5+, the semantics of volatile have been strengthened to be almost as strong as synchronized with respect to memory visibility and instruction ordering (see http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile). For the purposes of visibility, each access to a volatile field acts like half a synchronization.So, now both forms of memory barrier (under the current JMM) cause an instruction re-ordering barrier which prevents the compiler or run-time from re-ordering instructions across the barrier. In the old JMM, volatile did not prevent re-ordering. This can be important, because apart from memory barriers the only limitation imposed is that, for any particular thread, the net effect of the code is the same as it would be if the instructions were executed in precisely the order in which they appear in the source.
One use of volatile is for a shared but immutable object which is recreated on the fly, with many other threads taking a reference to the object at a particular point in their execution cycle. One needs the other threads to begin using the recreated object once it is published, but does not need the additional overhead of full synchronization and it's attendant contention and cache flushing.
Speaking to your read-update-write question, specifically. Consider the following unsafe code:
Now, with the updateCounter() method unsynchronized, two threads may enter it at the same time. Among the many permutations of what could happen, one is that thread-1 does the test for counter==1000 and finds it true and is then suspended. Then thread-2 does the same test and also sees it true and is suspended. Then thread-1 resumes and sets counter to 0. Then thread-2 resumes and again sets counter to 0 because it missed the update from thread-1. This can also happen even if thread switching does not occur as I have described, but simply because two different cached copies of counter were present in two different CPU cores and the threads each ran on a separate core. For that matter, one thread could have counter at one value and the other could have counter at some entirely different value just because of caching.
What's important in this example is that the variable counter was read from main memory into cache, updated in cache and only written back to main memory at some indeterminate point later when a memory barrier occurred or when the cache memory was needed for something else. Making the counter
volatile
is insufficient for thread-safety of this code, because the test for the maximum and the assignments are discrete operations, including the increment which is a set of non-atomicread+increment+write
machine instructions, something like:Volatile variables are useful only when all operations performed on them are "atomic", such as my example where a reference to a fully formed object is only read or written (and, indeed, typically it's only written from a single point). Another example would be a volatile array reference backing a copy-on-write list, provided the array was only read by first taking a local copy of the reference to it.
http://javaexp.blogspot.com/2007/12/difference -在-挥发性-和.html之间
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
多线程有 3 个主要问题:
竞争条件
缓存/陈旧内存
编译器和CPU优化
易失性
可以解决2& 3、但是解决不了1。synchronized
/显式锁可以解决1、2& 3.详细说明:
x++;
虽然它可能看起来像一个操作,但它实际上是 3:从内存中读取 x 的当前值,将 1 添加到并将其保存回内存中。如果几个线程尝试同时执行此操作,则操作的结果是不确定的。如果 x 最初是 1,那么在 2 个线程操作代码之后,它可能是 2,也可能是 3,具体取决于哪个线程在控制权转移到另一个线程之前完成了操作的哪一部分。这是一种竞争条件。
在代码块上使用
synchronized
使其成为原子 - 这意味着它使得 3 个操作就像同时发生,并且另一个线程无法介入其中并干涉。因此,如果x
为 1,并且 2 个线程尝试执行x++
,我们知道最终将等于 3。因此它解决了竞争条件问题。将
x
标记为易失性
不会使x++;
原子化,因此它不能解决此问题。考虑在一个线程上,
x = 10;
。稍后,在另一个线程中,x = 20;
。x
值的更改可能不会出现在第一个线程中,因为另一个线程已将新值保存到其工作内存中,但尚未将其复制到主内存中。或者它确实将其复制到主内存,但第一个线程尚未更新其工作副本。因此,如果现在第一个线程检查if (x == 20)
,答案将为false
。将变量标记为“易失性”基本上告诉所有线程仅在主内存上执行读写操作。 Synchronized 告诉每个线程在进入块时从主内存更新其值,并在退出块时将结果刷新回主内存。
请注意,与数据竞争不同,过时的内存并不那么容易(重新)生成,因为无论如何都会发生主内存的刷新。
考虑以下代码:
您可能认为 threadB 只能打印 20(或者如果在将
b
设置为 true 之前执行 threadB if-check,则根本不打印任何内容),如b
仅在x
设置为 20 后才设置为 true,但编译器/CPU 可能决定重新排序 threadA,在这种情况下 threadB 也可以打印 10。将b
标记为易失性
确保它不会被重新排序(或在某些情况下被丢弃)。这意味着 threadB 只能打印 20(或者什么也不打印)。将方法标记为同步将获得相同的结果。另外,将变量标记为 易失性 只能确保它不会被重新排序,但它之前/之后的所有内容仍然可以重新排序,因此同步可能更适合某些场景。请注意,在 Java 5 新内存模型之前,易失性并没有解决这个问题。
There are 3 main issues with multithreading:
Race Conditions
Caching / stale memory
Compiler and CPU optimisations
volatile
can solve 2 & 3, but can't solve 1.synchronized
/explicit locks can solve 1, 2 & 3.Elaboration:
x++;
While it may look like one operation, it's actually 3: reading the current value of x from memory, adding 1 to it, and saving it back to memory. If few threads try to do it at the same time, the result of the operation is undefined. If
x
originally was 1, after 2 threads operating the code it may be 2 and it may be 3, depending on which thread completed which part of the operation before control was transferred to the other thread. This is a form of race condition.Using
synchronized
on a block of code makes it atomic - meaning it make it as if the 3 operations happen at once, and there's no way for another thread to come in the middle and interfere. So ifx
was 1, and 2 threads try to preformx++
we know in the end it will be equal to 3. So it solves the race condition problem.Marking
x
asvolatile
does not makex++;
atomic, so it doesn't solve this problem.Consider that on one thread,
x = 10;
. And somewhat later, in another thread,x = 20;
. The change in value ofx
might not appear in the first thread, because the other thread has saved the new value to its working memory, but hasn't copied it to the main memory. Or that it did copy it to the main memory, but the first thread hasn't updated its working copy. So if now the first thread checksif (x == 20)
the answer will befalse
.Marking a variable as
volatile
basically tells all threads to do read and write operations on main memory only.synchronized
tells every thread to go update their value from main memory when they enter the block, and flush the result back to main memory when they exit the block.Note that unlike data races, stale memory is not so easy to (re)produce, as flushes to main memory occur anyway.
Consider the following code:
You would think that threadB could only print 20 (or not print anything at all if threadB if-check is executed before setting
b
to true), asb
is set to true only afterx
is set to 20, but the compiler/CPU might decide to reorder threadA, in that case threadB could also print 10. Markingb
asvolatile
ensures that it won’t be reordered (or discarded in certain cases). Which mean threadB could only print 20 (or nothing at all). Marking the methods as syncrhonized will achieve the same result. Also marking a variable asvolatile
only ensures that it won’t get reordered, but everything before/after it can still be reordered, so synchronization can be more suited in some scenarios.Note that before Java 5 New Memory Model, volatile didn’t solve this issue.
synchronized
是用于保护方法或代码块的关键字。通过将方法设为同步,您可以实现两件事。
volatile
是变量访问修饰符,它强制所有线程获取该对象的最新值来自主存的变量。所有线程都可以同时访问易失性变量值,而无需任何锁定。使用易失性变量的一个很好的例子:
Date
变量。假设您已将 Date 变量设为
易失性
。您不需要不同的线程为同一变量显示不同的时间。访问该变量的所有线程始终从主内存获取最新数据,以便所有线程显示真实的(实际)日期值。Lawrence Dol 清楚地解释了您的
读写更新查询
。关于您的其他查询
什么时候声明变量 volatile 比通过同步访问它们更合适?
如果您认为所有线程都应该获取变量的实际值,则必须使用
volatile
像上面介绍的数据示例一样实时。对于依赖于输入的变量使用 volatile 是一个好主意吗?
答案将与第一个查询中的相同。
synchronized
is keyword used to guard a method or code block. By making method as synchronized , you achieve two things.
volatile
is variable access modifier which forces all threads to get latest value of the variable from main memory. All threads can access volatile variable value at same time with out any locks.A good example to use volatile variable :
Date
variable.Assume that you have made Date variable
volatile
. You don't need different threads showing different time for same variable. All the threads, which access this variable always get latest data from main memory so that all threads show real (actual) Date value.Lawrence Dol cleary explained your
read-write-update query
.Regarding your other queries
When is it more suitable to declare variables volatile than access them through synchronized?
You have to use
volatile
if you think all threads should get actual value of the variable in real time like Data example covered above.Is it a good idea to use volatile for variables that depend on input?
Answer will be same as in first query.