2016-01-08 26 views
3

我读了下面的程序并在博客中回答。同步关键字如何在内部工作

int x = 0; 
boolean bExit = false; 

线程1(不同步)

x = 1; 
bExit = true; 

线程2(不同步)

if (bExit == true) 
System.out.println("x=" + x); 

有可能是线程2打印“x=0”?
:是的(原因:每个线程都有自己的变量副本)。

你怎么解决?
Ans:通过使两个线程同步在一个共同的互斥体上或使两个变量易变。

我的疑问是:如果我们将2变量设置为volatile,那么2个线程将共享来自主存储器的变量。这是有道理的,但在同步的情况下,它将如何解决,因为线程都有自己的变量副本。

请帮帮我。

+4

*“如果我们做了2变量波动,那么2个线程将分享从主内存中的变量。” * - 他们好歹“共享”中的“主存储器”(即使他们不挥发)变量。当使用“synchronized”块时,他们也会“共享”这些变量。关键的问题是一个线程是否可以看到另一个线程所做的修改。这可以通过各种方式实现,例如,与'volatile'或'synchronized'块。 – Marco13

+0

顺便说一句,如果你得到相同的结果,CPU不必在主存中存储'volatile'字段。即它实际上将多个副本存储在CPU高速缓存中,但是每当它被改变时它就被广播到也具有副本的高速缓存。 –

+0

“每个线程都有自己的变量副本”是无稽之谈。 –

回答

8

这实际上比看起来更复杂。工作中有几件神秘的事情。

缓存

说“每个线程都有自己的变量副本”是不完全正确的。每个线程可能有自己的变量副本,他们可能会也可能不会将这些变量刷新到共享内存中和/或从那里读取它们,因此整个事情是非确定性的。此外,冲洗这个名词实际上是依赖于实现的。有严格的术语,如内存一致性,发生在订单之前,同步订单

重新排序

这一个是更加神秘。这

x = 1; 
bExit = true; 

甚至不保证线程1将首先写1x然后truebExit。事实上,它甚至不保证任何这些都会发生。如果稍后不使用,编译器可能优化掉一些值。编译器和CPU也允许以任何他们想要的方式对指令进行重新排序,只要结果与如果一切确实在程序订单中会发生的情况无法区分。就是说,对于当前线程来说难以区分!没有人关心其他线程,直到......

同步进来

同步并不仅仅意味着对资源的独占访问。这也不仅仅是防止线程互相干扰。这也是关于记忆障碍。它可以粗略地描述为每个同步块在入口和出口处都有不可见指令,第一个同步块说“从共享内存中读取所有内容尽可能最新”,最后一个说“现在刷新你的任何内容在那里共享内存“。我说“粗略”,因为整个事情又是一个实现细节。内存障碍也会限制重新排序:操作可能仍会重新排序,但退出同步块后出现在共享内存中的结果必须与如果所有内容确实按程序顺序发生的情况相同。

只有当两个块都使用相同的锁定对象时,所有这些才有效。

整个事情在JLS的Chapter 17中有详细描述。尤其重要的是所谓的“先发后发”。如果你在文档中看到“这个发生了 - 在之前”,这意味着“this”之前的第一个线程所做的所有事情对任何执行“那个”的人都是可见的。这甚至可能不需要任何锁定。并发集合是一个很好的例子:一个线程放置了一些东西,另一个线程放置了一些东西,另一个线程读取了这个东西,并且神奇地保证第二个线程在将该对象放入集合之前会看到第一个线程所做的所有事情,即使这些操作与无关集合本身!

volatile变量

最后一个警告:你最好放弃的想法,使变量volatile就能解决的事情。在这种情况下,制作bExit挥发性就足够了,但是有太多的麻烦,使用挥发性物质会导致我甚至不愿意进入。但有一件事是肯定的:使用​​比使用volatile的效果强得多,这也适用于记忆效应。更糟糕的是,volatile语义在某些Java版本中发生了变化,因此可能存在一些仍旧使用旧语义的版本,这些旧语义更加模糊和混乱,而​​只要您了解它是什么以及如何使用它,就可以运行良好。

几乎所有使用volatile的唯一原因是性能,因为​​可能会导致锁争用和其他问题。阅读Java Concurrency in Practice以了解全部内容。

Q &一个

1)你写的 “现在刷新不管你一直在做有到共享内存 ” 关于synchronized块。但是我们只会看到我们在同步块中访问的变量 或者线程调用同步所做的所有更改(即使对于未在 同步块中访问的变量)?

简短回答:它将“刷新”在同步块期间或进入同步块之前更新的所有变量。再次,因为刷新是一个实现细节,你甚至不知道它是否会实际上刷新某些东西或者做一些完全不同的事情(或者根本没有做任何事情,因为执行和特定的情况已经以某种方式保证它可以工作)。

在同步块内未访问的变量显然不会在块的执行过程中发生变化。但是,例如,如果在进入同步块之前更改其中一些变量,那么在这些更改与发生在同步块(17.4.5中的第一个项目符号)中的任何变化之间都会有一个发生之前的关系。如果某个其他线程使用同一个锁对象输入另一个同步块,则它将与第一个线程退出同步块同步,这意味着此处有另一个发生在此之前的关系。所以在这种情况下,第二个线程看到第一个线程在进入同步块之前更新的变量。

如果第二个线程试图在没有相同的锁同步读取这些变量,那么就不能保证看到更新。但是再次,不能保证看到在同步块内进行的更新。但这是因为第二个线程中缺少内存读取屏障,并不是因为第一个没有“刷新”其变量(内存写入屏障)。

2)在本章中,您发布(的JLS)则撰文指出:“到 挥发性场的写入(§8.3.1.4)之前发生那场 的每一个后续读。”这难道不意味着当变量是挥发性的,你会看到 它只是改变(因为它是书面写的之前发生 读,而不是之前发生它们之间的每一个操作!)。我的意思是 不这是不是意味着在本例中,在 问题的说明中给出,我们可以在第二个线程看到bExit =真实的,但X = 0,如果 只有bExit是volatile吗?我问,因为我在这里找到这个问题:http://java67.blogspot.bg/2012/09/top-10-tricky-java-interview-questions-answers.html 它写道,如果bExit是易失性的程序是确定的。那么 寄存器将只刷新bExits值还是bExits和x值?

通过同样的道理在Q1,如果你做bExit = truex = 1后,再有一个在线程的之前发生关系,因为按照程序顺序的。现在,由于易失性写入发生 - 在易失性读取之前,保证第二个线程将看到在写入truebExit之前更新的第一个线程。请注意,这种行为仅在Java 1.5版本以后才有,因此旧版或者多版本的实现可能会支持或不支持。我已经在标准的Oracle实现中看到了一些使用这个特性的东西(java.concurrent collections),所以你至少可以假设它在那里工作。

3)为什么使用关于内存 可见性的同步块监视事项?我的意思是,当试图退出synchronized块是不是所有 变量(我们在此块访问或在 线程的所有变量 - 这是关系到的第一个问题)从寄存器 到主内存刷新或广播到所有CPU的缓存?为什么对象 同步很重要?我无法想象什么是关系,并且它们是如何制造的(在同步对象和内存之间)。 我知道我们应该使用相同的监视器看到这个变化,但我 不理解记忆,应该是可见的是如何映射到 对象。对不起,对于很长的问题,但这些对我来说真的是有趣的问题,并且它与问题有关(我的 会发布完全针对此引用的问题)。

哈,这个人是真的很有趣。我不知道。可能无论如何它会刷新,但是Java规范是高度抽象的编写的,所以也许它允许一些非常奇怪的硬件,其中部分刷新或其他类型的内存障碍是可能的。假设你在每个CPU上有一个双CPU的机器,内含2个内核。每个CPU对每个核心都有一些本地缓存,也有一个公共缓存。一个非常聪明的VM可能想要在一个CPU上安排两个线程,在另一个CPU上安排两个线程。每对线程都使用自己的监视器,并且VM检测到由这两个线程修改的变量不会在任何其他线程中使用,因此只会将它们刷新到CPU本地缓存。

又见this question同一个问题。

4)我认为写挥发性之前一切都将达到 日,当我们读它(而且当我们使用挥发性的读取,在 Java的是记忆障碍),但文档不说这个。

它的作用:

17.4.5。 如果x和y是同一个线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)。

如果HB(X,Y)和HB(Y,Z),则HB(X,Z)。

于挥发性字段(§8.3.1.4)写入每个后续 读取该字段的之前发生。

如果x = 1按照程序顺序来bExit = true之前,那么我们的之前发生在它们之间。如果其他线程在此之后读取bExit,那么我们在写入和读取之间发生。并且由于传递性,我们也发生了 - 在x = 1之前和第二个线程的bExit的读取之间。

5)此外,如果我们有挥发性人员P确实,我们有一些依赖性 当我们使用p.age = 20和打印(p.age)或具有我们存储器屏障 这种情况下(假设年龄是不易变)? - 我认为 - 没有

你是正确的。由于age不易变,所以没有内存障碍,这是最棘手的事情之一。下面是从CopyOnWriteArrayList的片段,例如:

 Object[] elements = getArray(); 
     E oldValue = get(elements, index); 
     if (oldValue != element) { 
      int len = elements.length; 
      Object[] newElements = Arrays.copyOf(elements, len); 
      newElements[index] = element; 
      setArray(newElements); 
     } else { 
      // Not quite a no-op; ensures volatile write semantics 
      setArray(elements); 

这里,getArraysetArray是微不足道setter和getter为array字段。但是,由于代码更改了数组的元素,因此必须将对该数组的引用写回到它所来自的位置,以便数组元素的更改可见。请注意,即使被替换的元素与首先存在的元素相同,也会执行该操作!正是因为该元素的某些字段可能已被调用线程所改变,因此有必要将这些更改传播给未来的读者。

6)是否有任何发生在易失性 字段的后续读取?我的意思是它第二次读取将看到线程 这之前,它读取这个领域的所有变化(当然,我们将不得不改变只有 如果之前这一切的变化挥发影响的可见性 - 这我 有点困惑不管是真的还是不)?

没有,有挥发性之间读取没有关系。当然,如果一个线程执行易失性写操作,然后另外两个线程执行易失性读操作,它们将保证至少可以看到与易失写操作之前一样的所有内容,但不能保证一个线程是否会看到更多最新的值比另一个。而且,一个易失性读取甚至没有严格的定义发生在另一个之前!想想在一个单一的全球时间表上发生的一切都是错误的。它更像是具有独立时间线的并行Universe,有时通过执行同步并与存储器屏障交换数据来同步时钟。

+0

优秀的解释。我有几个问题,因为我不确定我是否理解了一些正确的东西。 1)你写'现在刷新你已经在那里共享内存'关于同步块。但是我们只会看到我们在同步块中访问的变量或者线程调用同步所做的所有更改(即使对于未在同步块中访问的变量)? – DPM

+0

2)在本章中,您发布(的JLS)则撰文指出:'易挥发物场的写入(§8.3.1.4)之前发生的是field.'以后的每一次读取。这不意味着当变量是易失性的时候,你将只能看到它的变化(因为它在写入之前发生 - 在读取之前发生 - 不发生 - 在它们之间的每个操作之前)。我的意思是,这并不意味着在例子中给出的问题描述中,如果只有bExit是易失性的,我们可以看到bExit = true,但在第二个线程中x = 0。 – DPM

+0

2 - 继续)我问,因为我在这里找到了这个问题:'HTTP:// java67.blogspot.bg/2012/09 /顶10棘手的Java面试疑问的answers.html',它被写入如果bExit是易失性的,那么程序是OK的。所以寄存器只会刷新bExits值或bExits和x值? – DPM