Java中volatile和synchronized的区别

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

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

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

...

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

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

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

126441 次浏览

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

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

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

使用synchronized可以防止任何其他线程获得监视器(或锁)对于相同的对象 ,从而防止同步在同一对象上 保护的所有代码块并发执行。同步创建一个&;happens-before&;内存屏障,导致内存可见性限制,这样在某个线程释放锁出现到另一个线程随后获得相同的锁之前所做的任何事情都发生在它获得锁之前。实际上,在当前硬件上,这通常会在获取监视器时导致CPU缓存的刷新,并在释放监视器时写入主存,这两种操作(相对而言)都很昂贵。

另一方面,使用volatile将强制对易失性变量的所有访问(读或写)发生在主存中,有效地将易失性变量排除在CPU缓存之外。这对于某些仅仅要求变量可见性正确且访问顺序不重要的操作非常有用。使用volatile还改变了对longdouble的处理,要求对它们的访问是原子的;在一些(较旧的)硬件上,这可能需要锁,但在现代64位硬件上不需要。在Java 5+的新(JSR-133)内存模型下,volatile的语义在内存可见性和指令顺序方面得到了加强,几乎与synchronized一样强(参见http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile)。出于可见性的目的,对volatile字段的每次访问都相当于同步的一半。

在新的内存模型下,volatile变量之间仍然不能重新排序。不同的是,现在不再那么容易重新排序周围的正常字段访问。写入volatile字段具有与释放监视器相同的内存效果,从volatile字段读取具有与获取监视器相同的内存效果。实际上,由于新的内存模型对volatile字段访问与其他字段访问(volatile或非volatile)的重排序施加了更严格的约束,线程A写入volatile字段f时可见的任何内容在线程B读取f时都变得可见。

——JSR 133 (Java内存模型)FAQ

所以,现在这两种形式的内存障碍(在当前的JMM下)都会导致指令重新排序障碍,从而阻止编译器或运行时通过该障碍对指令重新排序。在旧的JMM中,volatile不能阻止重新排序。这可能很重要,因为除了内存障碍之外,唯一施加的限制是对于任何特定的线程,代码的净效果与指令按照它们在源代码中出现的顺序精确执行是相同的。

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++; }
}

现在,由于updateCounter()方法不同步,两个线程可以同时进入该方法。在可能发生的许多排列中,一种是线程-1对counter==1000进行测试,发现它为真,然后挂起。然后线程2执行相同的测试,也认为它为真并被挂起。然后线程-1恢复并将计数器设置为0。然后线程2恢复并再次将计数器设置为0,因为它错过了线程1的更新。即使线程切换没有像我所描述的那样发生,也会发生这种情况,只是因为两个不同的CPU内核中存在两个不同的缓存副本,并且每个线程都在单独的内核上运行。因此,由于缓存的原因,一个线程可以在一个值上有计数器,而另一个线程可以在另一个完全不同的值上有计数器。

在这个例子中,重要的是变量计数器从主存读入缓存,在缓存中更新,只有在稍后某个不确定的时间点,当发生内存障碍或当缓存内存需要做其他事情时,才会写回主存。设置计数器volatile对于这段代码的线程安全性是不够的,因为对最大值的测试和赋值是离散操作,包括增量操作,它是一组非原子的read+increment+write机器指令,例如:

MOV EAX,counter
INC EAX
MOV counter,EAX

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

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

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


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


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

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

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

波动性和同步性之间有两个区别。

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

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

因此,volatile只同步线程内存和“主”内存之间的一个变量的值,synchronized同步线程内存和“主”内存之间的所有变量的值,并锁定并释放一个监控器以引导。显然,synchronized可能比volatile有更多的开销。

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

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

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

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

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

enter image description here

为了更好地理解volatile的概念,看看这个文章

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

关于你的其他问题

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

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

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

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

为了更好地理解,请参考这个文章

多线程有3个主要问题:

  1. < p >竞态条件

  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,并且有两个线程尝试执行x++,则知道最终将等于3。所以它解决了竞争条件问题。

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

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

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

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

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

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

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

考虑下面的代码:

boolean b = false;
int x = 10;


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


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

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

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