提问者:小点点

理解Java中同步块与易失性变量的原子性、可见性和重新排序


我试图从《Java并发实践》一书中理解flash关键字。我在原子性、波动性和重新排序三个方面比较了同步关键字和易失性变量。我对此有些怀疑。我在下面一一讨论了它们:

书中关于同步的可见性如下:

当线程 A 在执行由同一锁保护的同步块时,线程 A 在同步块中或之前所做的一切都对 B 可见。

关于< code>volatile变量的可见性,它说明如下:

Volatile变量不缓存在寄存器或缓存中,在缓存中它们对其他处理器是隐藏的,因此读取volatile变量总是返回任何线程最近的写操作。< br > volatile变量的可见性影响超出了volatile变量本身的值。当线程A写入可变变量,随后线程B读取相同的变量时,在写入可变变量之前对A可见的所有变量的值在读取可变变量之后变得对B可见。所以从内存可见性的角度来看,写一个可变变量就像退出一个同步块,读一个可变变量就像进入一个同步块。

问题1.我觉得上面的第二段(volatile)与书中关于同步的内容相对应。但是,是否有同步——相当于volatile的第一段?换句话说,使用<code>synchronized</code>是否确保任何/某些变量不会缓存在处理器缓存和寄存器中?

请注意,书中还谈到了同步的可见性:

锁定不仅仅是相互排斥;它还与内存可见性有关。

这本书在重新排序的背景下介绍了以下关于<code>volatile

当一个字段被声明为flash时,编译器和运行时会注意到这个变量是共享的,并且对它的操作不应该与其他内存操作重新排序。

Q2.Book没有说明在同步的上下文中的重新排序。有人能解释一下在<code>synchronized

书中提到了同步flash的原子性。

volatile的语义不够强大,不足以使增量操作(< code>count )成为原子操作,除非您可以保证该变量只从单个线程中写入。

锁定可以保证可见性和原子性;可变变量只能保证可见性。

Q3。我猜这意味着两个线程可以一起看到< code>volatile int a,两个线程都会递增它,然后保存它。但是只有最后一次读取才会生效,因此整个“读取-增量-保存”不是原子性的。我对< code>volatile的非原子性的解释正确吗?

Q4.所有等价的锁是否都是可比较的,并且具有相同的可见性、排序和原子性属性:同步块、原子变量、锁?

PS:这个问题与我几天前问的这个问题有关,并且完全修改了版本。自从它完全修改以来,我没有删除旧的。我以更集中和更有条理的方式写了这个问题。一旦我得到这个问题的答案,我会删除旧的。


共3个答案

匿名用户

“synchronized”和“volatile”的主要区别在于,“synchronized”可以让线程暂停,而volatile不能。

“缓存和寄存器”不是一个东西。这本书说,因为在实践中,事情通常是这样实现的,这使得理解如何实现更容易(或者不容易,考虑到这些问题)

然而,JMM没有给它们命名。它只是说,VM可以自由地给每个线程任何变量的本地副本,或者不可以,在任意时间与一些或所有其他线程同步,或者不同步...除非在任何地方都存在先发生后关系,在这种情况下,VM必须确保在两个线程之间的执行点,即先发生后关系已经建立的地方,它们观察到处于相同状态的所有变量。

在实践中,这可能意味着刷新缓存。或者不;这可能意味着另一个线程会覆盖其本地副本。

虚拟机可以随心所欲地实现这些东西,而且在每个体系结构上都有所不同。只要虚拟机坚持JMM所做的保证,这就是一个很好的实现,因此,您的软件必须在只有这些保证而没有其他假设的情况下工作;因为如果你依赖于JMM无法保证的假设,那么在你的机器上工作的东西可能在另一台机器上不起作用。

重新排序也根本不在 VM 规范中。VM 规范中的 IS 是以下两个概念:

>

  • 在单个线程的范围内,您可以从内部观察到的所有内容都与有序视图一致。也就是说,如果你写'x=5;y=10;'在同一个线程中,不可能观察到y是10,但x是它的旧值。不管是同步的还是易失的。所以,任何时候它都可以在不可观察的情况下重新排序,那么虚拟机就可以了。会吗?直到虚拟机。有些人会,有些人不会。

    当观察由其他线程引起的影响,并且您尚未建立发生之前关系时,您可能会以任何顺序看到一些、所有或没有这些影响。真的,这里什么都有可能发生。在实践中,那么:不要试图观察由其他线程引起的影响,如果没有建立一个发生之前,因为结果是任意的和不可测试的。

    发生——在各种事情建立关系之前;同步块显然可以做到这一点(如果您的线程在尝试获取锁时被冻结,然后它运行,那么您现在可以观察到该对象上任何“以前发生过”的同步块,以及它们所做的任何事情,同时保证您观察到的内容与那些按顺序运行的内容一致,并且它们写入的所有数据您都可以看到(例如,您不会获得较旧的“缓存”或诸如此类的内容)。易失性访问也是如此。

    是的,你对为什么x不是原子的解释是正确的,即使x是易变的。

    我不知道你的第四季度想问什么。

    一般来说,如果您想以原子方式递增整数,或者执行任何其他并发类型的操作,请查看java. util.connow包。这些包含各种概念的有效且有用的实现。例如,原子整数可用于以其他线程可见的方式以原子方式递增某些内容,同时仍然非常高效(例如,如果您的CPU支持比较和设置(CAS)操作,原子整数将使用它;这不是您可以在不求助于不安全的情况下从一般java中做的事情)。

  • 匿名用户

    只是为了补充rzwitserloot优秀的答案:

    A1。你可以这样想:同步保证一旦第一个线程退出同步块,在另一个线程进入之前,所有兑现的更改都将对进入同步块的其他线程可见(从缓存中刷新)。

    回答 2.当且仅当 T2 在同一防护上同步时,同步块中的线程 T1 执行的操作对其他线程 T2 显示为未重新排序。

    A3。我不知道你对此有什么理解。可能发生的情况是,当递增两个线程时,首先会读取变量a,这将产生一些值v,然后两个线程都会在本地增加其值v的本地副本,产生v'=v 1,然后两个线程都会将v'写入a。因此最后a的值可能是v 1而不是v 2

    回答 4.基本上是的,尽管在同步块中您可以原子地执行许多操作,而原子变量只允许您执行某个单个操作,例如原子增量。此外,不同之处在于,当错误地使用同步块时,即通过读取同步块外部的变量,这些变量被同步块中的另一个线程修改,您可以不原子地观察它们并重新排序。原子变量不可能实现的事情。锁定与同步完全相同。

    匿名用户

    问题1.我觉得上面(volatile)的第二段与书中所说的同步一致。

    当然可以。< code>volatile访问可以被视为同步lite。

    但是有没有synchronized——相当于volatile的第一段?换句话说,使用synchronized是否能确保某些变量不会缓存在处理器缓存和寄存器中?

    这本书混淆了层次,把你弄糊涂了volatile访问与处理器缓存或寄存器没有直接关系,事实上,这本书对缓存的描述肯定是不正确的。波动性和同步性是关于某些操作的线程间可见性,尤其是对共享变量的写入。语义是如何实现的在很大程度上是一个单独的问题。

    在任何情况下,不,同步不会对变量的存储施加任何约束。与同步语义有关的一切都发生在同步区域的边界。这就是为什么一组并发运行的线程对给定变量的所有访问必须在同一对象上同步,以便程序相对于该变量正确同步。

    这本书在重新排序的背景下介绍了以下关于<code>volatile

    当一个字段被声明为flash时,编译器和运行时会注意到这个变量是共享的,并且对它的操作不应该与其他内存操作重新排序。

    但是这已经说明了同步访问的一些问题(不是全部)。您需要理解,这种意义上的“内存操作”是指读取或写入共享变量,或者获取或释放任何对象的监视器。进入一个同步区域需要获得一个监视器,所以这本书已经正确地说明了< code>volatile访问不会跨同步区域的边界重新排序。

    更一般地说,共享变量的读取将不会相对于同步区域的开始重新排序,而写入将不会相对于一个区域的结束重新排序。

    Q3。我想这意味着两个线程可以一起看到易失性int a,两者都会增加它,然后保存它。但是只有最后一次读取会产生影响,从而使整个“读取-增量-保存”成为非原子的。我对易失性的非原子性的解释是正确的吗?

    是的。自动递增运算符对应用它的变量执行读取和写入。如果该变量是flash,则其单独应用于这些变量,因此如果没有其他保护,则可以在两者之间对同一变量进行其他操作。

    Q4.所有等价的锁是否都是可比较的,并且具有相同的可见性、排序和原子性属性:同步块、原子变量、锁?

    嗯?这个子问题太宽泛了。你正在读一本关于它的书。然而,总的来说,这些机制没有一些共同的特征,也没有一些不同的特征。所有这些都对内存操作的可见性及其顺序有影响,但它们并不相同。“原子性”是另外两个的函数。