synchronized关键字如何在内部工作

jav*_*ava 3 java multithreading

我阅读了以下程序并在博客中回答.

int x = 0;
boolean bExit = false;
Run Code Online (Sandbox Code Playgroud)

线程1(未同步)

x = 1; 
bExit = true;
Run Code Online (Sandbox Code Playgroud)

线程2(未同步)

if (bExit == true) 
System.out.println("x=" + x);
Run Code Online (Sandbox Code Playgroud)

线程2是否可以打印" x=0"?
Ans:是(原因:每个线程都有自己的变量副本.)

你怎么解决它?
Ans:通过使用make两个线程在一个公共互斥锁上同步或使两个变量都是volatile.

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

请帮我.

Ser*_*nov 10

这实际上比看起来更复杂.有几个神秘的东西在起作用.

高速缓存

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

重新排序

这个更加神秘.这个

x = 1; 
bExit = true;
Run Code Online (Sandbox Code Playgroud)

甚至不保证线程1会先写1x,然后truebExit.事实上,它甚至不能保证任何这些都会发生.如果稍后不使用它们,编译器可以优化掉一些值.编译器和CPU也可以按照他们想要的方式对指令进行重新排序,只要结果与所有内容都按程序顺序无法区分即可.也就是说,当前线程无法区分!在......之前没人关心其他线程

同步进来了

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

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

整个事情在JLS的第17章中有详细描述.特别重要的是所谓的"先发生顺序".如果你在文档中看到"这发生在之前 ",那就意味着第一个线程在"this"之前所做的一切都将对那些"那个"的人可见.这甚至可能不需要任何锁定.并发集合是一个很好的例子:一个线程放置一些东西,另一个读取它,并且神奇地保证第二个线程在将该对象放入集合之前将看到第一个线程所做的一切,即使这些操作与之无关收藏本身!

易变量

最后一个警告:你最好放弃让变量volatile解决问题的想法.在这种情况下,也许制造bExit挥发性就足够了,但是有很多麻烦使用挥发物会导致我甚至不愿意这样做.但有一件事是肯定的:使用synchronized比使用具有更强的效果volatile,这也是记忆效应.更糟糕的是,volatile语义在某些Java版本中发生了变化,因此可能存在一些仍然使用旧语义的版本,这些版本更加模糊和令人困惑,而synchronized如果您了解它是什么以及如何使用它,它总是很有效.

几乎唯一使用的原因volatile是性能,因为synchronized可能会导致锁争用和其他麻烦.阅读实践中的Java Concurrency以了解所有这些.

问答

1)你写了关于同步块的"现在刷新你在共享内存中所做的任何事情".但是我们只会看到我们在同步块中访问的变量或者线程调用同步所做的所有更改(即使是在synchronized块中未访问的变量)?

简短回答:它将"刷新"在同步块期间或进入同步块之前更新的所有变量.再次,因为刷新是一个实现细节,你甚至不知道它是否会实际冲洗某些东西或做一些完全不同的东西(或根本不做任何事情,因为实现和具体情况已经在某种程度上保证它会起作用).

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

如果第二个线程尝试读取这些变量而不同步同一个锁,则无法保证看到更新.但话说回来,并不能保证在同步块内部也能看到更新.但这是因为第二个线程中缺少内存读取障碍,不是因为第一个没有"刷新"其变量(内存写入障碍).

2)在本章中,您发布了(JLS),它写道:"在对该字段的每次后续读取之前发生对易失性字段的写入(第8.3.1.4节)." 这是不是意味着当变量是易失性时,你只会看到它的变化(因为写入写入是在读取之前发生的,而不是在它们之间的每次操作之前发生!).我的意思是,这不意味着在示例中,在问题的描述中给出,我们可以看到bExit = true,但是如果只有bExit是volatile的话,在第二个线程中x = 0?我问,因为我在这里找到了这个问题:http: //java67.blogspot.bg/2012/09/top-10-tricky-java-interview-questions-answers.html并写道如果bExit是易变的程序没关系 那么寄存器只会刷新bExits值或bExits和x值?

根据同样的理由在Q1,如果你这样做bExit = true x = 1,再有就是一个在线程的之前发生关系,因为按照程序顺序的.现在,因为挥发性写入发生,之前挥发读取,可以保证第二个线程会看到之前以书面形式无论第一个线程更新truebExit.请注意,此行为仅限于Java 1.5左右,因此较旧或错误的实现可能支持也可能不支持.我已经看到标准Oracle实现中使用此功能(java.concurrent集合)的位,因此您至少可以假设它在那里工作.

3)为什么在使用有关内存可见性的同步块时监控很重要?我的意思是当尝试退出synchronized块时,并非所有变量(我们在此块中访问或线程中的所有变量 - 这与第一个问题相关)从寄存器刷新到主存储器或广播到所有CPU缓存?为什么同步对象很重要?我无法想象什么是关系以及它们是如何形成的(在同步对象和内存之间).我知道我们应该使用相同的监视器来查看这些更改,但我不明白应该可见的内存是如何映射到对象的.对不起,对于很长的问题,但这些对我来说真的很有趣,而且与问题有关(我会准确地为这个引子发布问题).

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

另请参阅有关同一问题的问题.

4)我认为在编写volatile之前的所有内容在我们阅读时都是最新的(而且当我们使用volatile中的读取时,在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和print(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);
Run Code Online (Sandbox Code Playgroud)

在这里,getArray并且setArray是该array领域的琐碎的制定者和吸气剂.但是由于代码更改了数组的元素,因此有必要将对数组的引用写回到它来自的位置,以便对数组元素的更改变为可见.请注意,即使被替换的元素与首先存在的元素相同,也可以完成!正是因为调用线程可能改变了该元素的某些字段,并且有必要将这些更改传播给未来的读者.

6)在2次后续读取volatile字段之前是否有任何事情发生?我的意思是第二次读取会看到来自线程的所有更改在它之前读取此字段(当然,只有当volatile会影响所有更改之前的可见性时我们才会进行更改 - 我对它是否真实感到有点困惑)?

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