在Java中volatile和synchronized的区别

17 浏览
0 Comments

在Java中volatile和synchronized的区别

我在想Java中将变量声明为volatile和总是在synchronized(this)块中访问变量之间的差异?

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

我特别关注这段信息:

...

  • 访问一个volatile变量永远不会有阻塞的潜力:我们只是进行简单的读取或写入,所以与同步块不同,我们不会保持任何锁定;
  • 因为访问volatile变量从不持有锁定,所以对于我们想要将读取-更新-写入作为原子操作的情况来说是不适用的(除非我们准备“错过更新”);

他们所说的读取-更新-写入是什么意思?难道一个写操作不也是更新吗,还是他们仅仅意味着更新是依赖于读取的写操作?

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

admin 更改状态以发布 2023年5月21日
0
0 Comments

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有一个“主”内存的概念,这是保存变量当前“正确”值的内存。线程可以拥有变量的自己的数据副本,并且线程副本可以与“主”内存中不同。因此,实际上,如果thread1thread2都更新了,但这些更新值尚未传播到“主”内存或其他线程,则“主”内存可能具有i11值,thread1可能具有i12值,而thread2则具有i13值。

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

volatile和synchronized之间有两个区别。

首先,synchronized在监视器上获取和释放锁,这可以强制一次只有一个线程执行代码块。这是关于synchronized的相当知名的方面。但是,synchronized还同步内存。实际上,synchronized将线程内存与“主”内存的整个内容同步。因此,执行geti3()将执行以下操作:

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

因此,volatile仅在线程内存和“主”内存之间同步一个变量的值,而synchronized会在线程内存和“主”内存之间同步所有变量的值,并锁定和释放监视器。显然,synchronized可能具有比volatile更多的开销。

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

0
0 Comments

理解线程安全有两个方面必须要考虑。

  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)。就可见性而言,每个对易失字段的访问就像半个同步。

在新的内存模型下,易变变量仍不能与彼此重排序。不同之处在于,在易变域访问周围重新排序通常域访问的运作现在不再那么容易了。向易变域写入的行为与监视器释放相同,而从易变域读取的行为与监视器获取相同。实际上,由于新的内存模型对易变域访问与其他域访问重排序施加了更严格的限制,易变的或不易变的,所以当线程A写入易变域f时线程A可见的任何东西成为线程B读取f时线程B可见的。

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

因此,现在在当前JMM下,内存屏障的两种形式都会导致指令重新排序障碍,这可防止编译器或运行时程序在屏障上重新排序指令。在旧的JMM中,易变并不防止重排序。这可能很重要,因为除了内存屏障之外,所施加的唯一限制是对于任何特定的线程,代码的净效果与指令在源中出现的顺序相同。

易变的一种用途是为共享的并且不可变的对象在执行周期的某个特定点上重新创建的情况,有很多其他线程需要引用该对象。一旦发布重建的对象,其他线程就需要开始使用它,但不需要全面同步的额外开销及其相关竞争和高速缓存刷新。

// 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的测试并发现为true,然后被挂起。然后线程2进行相同的测试并且也看到为true并被挂起。然后线程1恢复并将counter设置为0。然后线程2恢复并再次将counter设置为0,因为它错过了线程1的更新。即使线程切换不是像我描述的这样,这也可能发生,但仅仅因为两个不同的CPU核心中存在两个不同的缓存副本的counter变量,而线程每个都在不同的核心上运行。就此而言,一条线程可以在一个值上拥有counter,并且另一条线程可以以完全不同的值拥有counter,仅仅因为缓存。

在这个例子中重要的是,变量counter是从主存中读取到缓存中的,然后在缓存中更新,并且只在某个不确定时间点后,当发生存储屏障或当缓存中的内存被用于其他用途时,才写回到主存中。将counter定义为volatile是不足以保证这段代码的线程安全性的,因为找到最大值和赋值是离散的操作,包括增量,这是一组非原子性的读取+增量+写入机器指令,类似于:

MOV EAX,counter
INC EAX
MOV counter,EAX

只有当对volatile变量执行的所有操作都是“原子的”时,volatile变量才有用,比如我的例子中只读取或写入对一个完整形成对象的引用(实际上这通常只从单个点写入)。另一个例子是支持按需复制列表的volatile数组引用,只要数组通过先取该引用的本地副本进行读取。

0