Java中volatile和synchronized之间的区别

Alb*_*ore 222 java multithreading volatile synchronized java-me

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

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

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

...

  • 对volatile变量的访问永远不会阻塞:我们只进行简单的读或写操作,因此与synchronized块不同,我们永远不会持有任何锁;
  • 因为访问volatile变量永远不会持有锁,所以它不适合我们想要读取update-write作为原子操作的情况(除非我们准备"错过更新");

read-update-write是什么意思?写入也不是更新,还是仅仅意味着更新是依赖于读取的写入?

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

Law*_*Dol 366

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

  1. 执行控制,和
  2. 记忆能见度

第一个与控制何时执行代码(包括执行指令的顺序)以及它是否可以并发执行有关,第二个与内存中对已完成内容的影响何时对其他线程可见有关.因为每个CPU在它和主内存之间有几级缓存,所以在不同CPU或内核上运行的线程在任何给定时刻都可以看到"内存"不同,因为允许线程获取并处理主内存的私有副本.

使用synchronized可防止任何其他线程获取同一对象的监视器(或锁定),从而防止同一对象上由同步保护的所有代码块同时执行.同步还会创建一个"发生在之前"的内存屏障,导致内存可见性约束,使得在某个线程释放锁定之前所做的任何事情都会出现在另一个线程中,随后在获取锁定之前获取相同的锁定.实际上,在当前硬件上,这通常导致在获取监视器时刷新CPU高速缓存并在释放时写入主存储器,这两者都是(相对)昂贵的.

使用volatile,而另一方面,迫使所有的访问(读或写)到易失性可变发生到主存储器,有效地把挥发性变量out CPU的高速缓存.这对于一些只需要变量的可见性正确且访问顺序不重要的操作非常有用.使用volatile也改变对它们的处理longdouble要求对它们的访问是原子的; 在某些(较旧的)硬件上,这可能需要锁定,但不适用于现代64位硬件.在Java 5+的新(JSR-133)内存模型下,volatile的语义已经增强到几乎与内存可见性和指令排序同步一样强(参见http://www.cs.umd.edu) /users/pugh/java/memoryModel/jsr-133-faq.html#volatile).出于可见性的目的,对易失性字段的每次访问都像半同步一样.

在新的内存模型下,挥发性变量无法相互重新排序.不同之处在于现在不再那么容易重新排序它们周围的正常字段访问.写入易失性字段与监视器释放具有相同的记忆效应,从易失性字段读取具有与监视器获取相同的记忆效应.实际上,因为新的内存模型对其他字段访问(易失性或非易失性)的易失性字段访问的重新排序施加了更严格的限制,所以A当线程在写入易失性字段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.
Run Code Online (Sandbox Code Playgroud)

请特别说明您的读 - 更新 - 写问题.考虑以下不安全的代码:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }
Run Code Online (Sandbox Code Playgroud)

现在,在updateCounter()方法未同步的情况下,两个线程可以同时输入它.在可能发生的许多排列中,一个是thread-1对counter == 1000进行测试并发现它为真,然后被暂停.然后thread-2执行相同的测试,并且看到它是真的并且被暂停.然后thread-1恢复并将counter设置为0.然后thread-2恢复并再次将counter设置为0,因为它错过了thread-1的更新.即使没有像我所描述的那样发生线程切换,也会发生这种情况,但仅仅因为两个不同CPU核心中存在两个不同的计数器缓存副本,并且每个线程都运行在一个单独的核心上.就此而言,一个线程可以在一个值处具有计数器而另一个线程可以仅仅因为缓存而具有一些完全不同的值.

在这个例子中重要的是变量计数器从主存储器读入高速缓存,在高速缓存中更新,并且仅在发生内存屏障时或者在其他情况下需要高速缓存存储器时才在某个不确定点写回主存储器.使计数器volatile不足以保证此代码的线程安全,因为最大值和赋值的测试是离散操作,包括增量,这是一组非原子read+increment+write机器指令,如:

MOV EAX,counter
INC EAX
MOV counter,EAX
Run Code Online (Sandbox Code Playgroud)

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

  • 非常感谢!计数器的示例很容易理解.然而,当事情变得真实时,它会有所不同. (5认同)
  • @MarianPaździoch:增量或减量不是读取**或**写入,而是读取**和**写入;它是读入寄存器,然后寄存器递增,然后写回内存。读取和写入是“单独”原子的,但多个此类操作则不是。 (3认同)
  • 因此,根据常见问题解答,*不*仅仅因为锁定获取*所做的操作*在解锁后变得可见,但*该线程所做的所有*操作都是可见的.甚至在锁定获取之前进行的操作. (2认同)

Ker*_*ğan 95

volatile是一个字段修饰符,而synchronized则修改代码块方法.因此,我们可以使用这两个关键字指定简单访问器的三种变体:

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

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

    int i3;
    synchronized int geti3() {return i3;}
Run Code Online (Sandbox Code Playgroud)

geti1()访问i1当前线程中当前存储的值.线程可以有变量的本地副本,并且数据不必与其他线程中保存的数据相同.特别是,另一个线程可能已i1在其线程中更新,但当前线程中的值可能与此不同更新的价值.事实上,Java有一个"主"内存的概念,这是保存变量当前"正确"值的内存.线程可以拥有自己的变量数据副本,并且线程副本可以与"主"内存不同.所以,事实上,可能的是"主"存储器具有的值1i1,对线程1到具有值2i1线程2如果thread1thread2都更新了i1但是那些更新的值尚未传播到"主"内存或其他线程,则值为3.i1

另一方面,geti2()有效地访问i2"主"存储器的值.不允许volatile变量具有与"main"内存中当前保存的值不同的变量的本地副本.实际上,声明为volatile的变量必须使其数据在所有线程之间同步,因此无论何时在任何线程中访问或更新变量,所有其他线程都会立即看到相同的值.通常,volatile变量比"plain"变量具有更高的访问和更新开销.通常,允许线程拥有自己的数据副本以提高效率.

volitile和synchronized之间有两个不同之处.

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

  1. 线程获取监视器上的对象锁定.
  2. 线程内存刷新所有变量,即它的所有变量都有效地从"主"内存中读取.
  3. 执行代码块(在这种情况下,将返回值设置为i3的当前值,该值可能刚刚从"main"存储器复位).
  4. (对变量的任何更改现在通常都会写入"主"内存,但对于geti3(),我们没有任何更改.)
  5. 该线程释放监视器上的锁定对象.

因此,volatile只在线程内存和"主"内存之间同步一个变量的值,synchronized会同步线程内存和"主"内存之间所有变量的值,并锁定和释放监视器以启动.明确同步可能比volatile更具开销.

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

  • -1,Volatile没有锁定,它使用底层CPU架构来确保写入后所有线程的可见性. (34认同)
  • 锁定volatile变量共享的重要语义是它们都提供Happens-Before边缘(Java 1.5及更高版本).输入一个同步块,取出一个锁并从一个volatile读取都被认为是"获取",并且释放一个锁,退出一个同步块并写一个volatile是所有形式的"释放". (2认同)

Rav*_*abu 19

synchronized是方法级别/块级访问限制修饰符.它将确保一个线程拥有临界区的锁.只有拥有锁的线程才能进入synchronized块状态.如果其他线程正在尝试访问此关键部分,则必须等到当前所有者释放锁定.

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

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

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

在此输入图像描述

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

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

关于你的其他疑问

什么时候更适合声明变量volatile而不是通过synchronized来访问它们?

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

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

答案与第一次查询中的答案相同.

请参阅本文以便更好地理解.


Dav*_*eli 5

tl; dr

多线程存在3个主要问题:

1)比赛条件

2)缓存/过时的内存

3)编译器和CPU优​​化

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

详细说明

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

x++;

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

synchronized在代码块上使用使它原子化 -意味着它使这3个操作立即发生,并且没有其他线程可以进入中间并进行干扰。因此,如果x为1,则有2个线程尝试执行预成型,x++我们最终知道它将等于3。因此,它解决了竞争条件问题。

synchronized (this) {
   x++; // no problem now
}
Run Code Online (Sandbox Code Playgroud)

标记x为as 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,在Java 5 New Memory Model之前,volatile无法解决此问题。