我想知道在Java中声明变量为volatile和总是在同步(this)块中访问变量之间的区别?

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

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

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

读-更新-写是什么意思?写不也是一个更新吗?或者他们仅仅意味着更新是一个取决于读的写?

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


当前回答

多线程有3个主要问题:

竞态条件 缓存/旧内存 编译器和CPU优化

Volatile可以解2和3,但不能解1。同步/显式锁可以解决1、2和3。

细化:

考虑以下线程不安全代码:

x++;

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

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

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

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

此外,线程有它们自己的上下文——也就是说,它们可以从主存中缓存值。这意味着一些线程可以拥有一个变量的副本,但它们对自己的工作副本进行操作,而不会在其他线程之间共享变量的新状态。

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

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

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

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

考虑下面的代码:

boolean b = false;
int x = 10;

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

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

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), as b is set to true only after x is set to 20, but the compiler/CPU might decide to reorder threadA, in that case threadB could also print 10. Marking b as volatile 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 as volatile 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.

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

其他回答

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

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

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

假设Date变量为volatile。访问此变量的所有线程总是从主存中获取最新数据,以便所有线程显示真实的Date值。你不需要不同的线程为相同的变量显示不同的时间。所有线程都应该显示正确的Date值。

为了更好地理解挥发性概念,可以阅读这篇文章。

Lawrence Dol cleary解释了你的读写更新查询。

关于你的其他问题

什么时候volatile声明变量比synchronized访问变量更合适?

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

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

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

请参考这篇文章以更好地理解。

多线程有3个主要问题:

竞态条件 缓存/旧内存 编译器和CPU优化

Volatile可以解2和3,但不能解1。同步/显式锁可以解决1、2和3。

细化:

考虑以下线程不安全代码:

x++;

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

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

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

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

此外,线程有它们自己的上下文——也就是说,它们可以从主存中缓存值。这意味着一些线程可以拥有一个变量的副本,但它们对自己的工作副本进行操作,而不会在其他线程之间共享变量的新状态。

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

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

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

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

考虑下面的代码:

boolean b = false;
int x = 10;

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

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

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), as b is set to true only after x is set to 20, but the compiler/CPU might decide to reorder threadA, in that case threadB could also print 10. Marking b as volatile 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 as volatile 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.

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

volatile is a field modifier, while synchronized modifies code blocks and methods. So we can specify three variations of a simple accessor using those two keywords: int i1; int geti1() {return i1;} volatile int i2; int geti2() {return i2;} int i3; synchronized int geti3() {return i3;} geti1() accesses the value currently stored in i1 in the current thread. Threads can have local copies of variables, and the data does not have to be the same as the data held in other threads.In particular, another thread may have updated i1 in it's thread, but the value in the current thread could be different from that updated value. In fact Java has the idea of a "main" memory, and this is the memory that holds the current "correct" value for variables. Threads can have their own copy of data for variables, and the thread copy can be different from the "main" memory. So in fact, it is possible for the "main" memory to have a value of 1 for i1, for thread1 to have a value of 2 for i1 and for thread2 to have a value of 3 for i1 if thread1 and thread2 have both updated i1 but those updated value has not yet been propagated to "main" memory or other threads. On the other hand, geti2() effectively accesses the value of i2 from "main" memory. A volatile variable is not allowed to have a local copy of a variable that is different from the value currently held in "main" memory. Effectively, a variable declared volatile must have it's data synchronized across all threads, so that whenever you access or update the variable in any thread, all other threads immediately see the same value. Generally volatile variables have a higher access and update overhead than "plain" variables. Generally threads are allowed to have their own copy of data is for better efficiency. There are two differences between volitile and synchronized. Firstly synchronized obtains and releases locks on monitors which can force only one thread at a time to execute a code block. That's the fairly well known aspect to synchronized. But synchronized also synchronizes memory. In fact synchronized synchronizes the whole of thread memory with "main" memory. So executing geti3() does the following: The thread acquires the lock on the monitor for object this . The thread memory flushes all its variables, i.e. it has all of its variables effectively read from "main" memory . The code block is executed (in this case setting the return value to the current value of i3, which may have just been reset from "main" memory). (Any changes to variables would normally now be written out to "main" memory, but for geti3() we have no changes.) The thread releases the lock on the monitor for object this. So where volatile only synchronizes the value of one variable between thread memory and "main" memory, synchronized synchronizes the value of all variables between thread memory and "main" memory, and locks and releases a monitor to boot. Clearly synchronized is likely to have more overhead than volatile.

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

线程安全有两个方面,理解这一点很重要。

执行控制,以及 记忆的可见性

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. Using volatile also changes treatment of long and double 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.

Under the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire. In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f. -- JSR 133 (Java Memory Model) FAQ

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.

volatile的一个用途是动态地重新创建一个共享但不可变的对象,许多其他线程在其执行周期的特定点获得对该对象的引用。一旦发布了重新创建的对象,就需要其他线程开始使用它,但不需要完全同步的额外开销以及随之而来的争用和缓存刷新。

// Declaration
public class SharedLocation {
    static public volatile SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using 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.
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++; }
    }

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-atomic read+increment+write machine instructions, something like:

MOV EAX,counter
INC EAX
MOV counter,EAX

Volatile变量只有在对它们执行的所有操作都是“原子的”时才有用,比如我的例子,其中对一个完全形成的对象的引用只被读写(实际上,通常只从一个点写入)。另一个例子是支持write -on-write列表的volatile数组引用,前提是该数组只能通过首先获取引用的本地副本来读取。