【问题标题】:how synchronized keyword works internallysynchronized 关键字在内部是如何工作的
【发布时间】:2016-04-13 01:41:27
【问题描述】:

我阅读了下面的程序并在博客中回答。

int x = 0;
boolean bExit = false;

线程 1(未同步)

x = 1; 
bExit = true;

线程 2(未同步)

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

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

如何解决它?
回答:通过使用使两个线程在一个公共互斥体上同步或使两个变量都可变。

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

请帮帮我。

【问题讨论】:

  • “如果我们将 2 变量设为 volatile,那么 2 个线程将共享主内存中的变量。” - 他们无论如何“共享”“main”中的变量内存”(即使它们不是易失性的)。当使用synchronized 块时,它们还将“共享”这些变量。关键问题是一个线程是否可以看到另一个线程所做的修改。这可以通过多种方式实现,例如带有volatilesynchronized 块。
  • 顺便说一句,CPU 不必在主内存中存储 volatile 字段,只要您得到相同的结果。也就是说,它实际上在 CPU 缓存中存储了多个副本,但是无论何时更改它都会广播到也有副本的缓存。
  • “每个线程都有自己的变量副本”是无稽之谈。

标签: java multithreading


【解决方案1】:

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

缓存

说“每个线程都有自己的变量副本”并不完全正确。每个线程可能都有自己的变量副本,它们可能会也可能不会将这些变量刷新到共享内存和/或从那里读取它们,所以整个事情是不确定的。此外,flushing 这个术语实际上是依赖于实现的。有严格的术语,例如内存一致性、happens-before 顺序同步顺序

重新排序

这个更神秘。这个

x = 1; 
bExit = true;

甚至不保证线程 1 会先将1 写入x,然后将true 写入bExit。事实上,它甚至不能保证这些都会发生。如果以后不使用某些值,编译器可能会优化掉它们。编译器和 CPU 也可以按照他们想要的任何方式重新排序指令,前提是结果与如果一切都真正按程序顺序进行时将发生的结果无法区分程序顺序。也就是说,对于当前线程无法区分!没有人关心其他线程,直到...

同步进来

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

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

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

易变变量

最后一个警告:你最好放弃创建变量volatile 会解决问题的想法。在这种情况下,也许让bExit volatile 就足够了,但是使用 volatile 会带来很多麻烦,我什至不愿意深入探讨。但有一件事是肯定的:使用synchronized 比使用volatile 具有更强的效果,这也适用于记忆效应。更糟糕的是,volatile 语义在某些 Java 版本中发生了变化,因此可能存在一些仍然使用旧语义的版本,这些语义更加晦涩难懂,而 synchronized 只要您了解它是什么以及如何使用它,就一直运行良好.

几乎使用volatile 的唯一原因是性能,因为synchronized 可能会导致锁争用和其他问题。阅读 Java 并发实践以了解所有这些。

问答

1) 你写了“现在将你在那里所做的一切刷新到共享 内存”关于同步块。但我们只会看到变量 我们在同步块中访问的或所有更改 线程调用同步进行(即使在未访问的变量上 同步块)?

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

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

如果第二个线程尝试读取这些变量而不在同一个锁上同步,则不能保证看到更新。但话又说回来,不能保证也能看到同步块内的更新。但这是因为第二个线程中缺少内存读取屏障,而不是因为第一个线程没有“刷新”其变量(内存写入屏障)。

2)在本章中,您(JLS)发布的内容是:“A write to a volatile 字段(第 8.3.1.4 节)发生在每次后续读取之前 字段。”这是否意味着当变量是 volatile 时,您将 只看到它的变化(因为它是写写发生前 阅读,而不是在他们之间的每次操作之前发生!)。我是说 这是否意味着在示例中,在描述中给出 问题,我们可以看到 bExit = true,但是在第二个线程中 x = 0 if 只有 bExit 易变?我问,因为我在这里找到了这个问题:http://java67.blogspot.bg/2012/09/top-10-tricky-java-interview-questions-answers.html 并且写到如果 bExit 是 volatile 程序是好的。所以 寄存器将仅刷新 bExits 值还是仅刷新 bExits 和 x 值?

根据与第一季度相同的推理,如果您执行bExit = true after x = 1,那么由于程序顺序,存在线程内发生之前的关系。现在,由于 volatile 写入发生在 volatile 读取之前,因此可以保证第二个线程将看到第一个线程在将 true 写入 bExit 之前更新的任何内容。请注意,此行为仅从 Java 1.5 开始,因此较旧或有错误的实现可能支持也可能不支持。我在标准 Oracle 实现中看到了一些使用此功能(java.concurrent 集合)的部分,因此您至少可以假设它在那里工作。

3) 为什么在使用有关内存的同步块时监控很重要 能见度?我的意思是当尝试退出同步块时并不是全部 变量(我们在此块中访问的或在 线程 - 这与第一个问题有关)从寄存器中刷新 到主存还是广播到所有 CPU 缓存?为什么对象 同步重要吗?我只是无法想象什么是关系和 它们是如何制作的(在同步对象和内存之间)。 我知道我们应该使用同一个监视器来查看这些变化,但我 不明白应该可见的内存是如何映射到的 对象。抱歉,问题很长,但这些确实是 对我来说有趣的问题,它与问题有关(我 会针对这本入门书发布问题)。

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

关于同一问题,另请参阅 this question

4) 我认为在写一个 volatile 之前的一切都会达到 我们阅读它的日期(此外,当我们使用 volatile a read 时 Java 它是内存屏障),但文档没有这样说。

确实如此:

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

如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。

对 volatile 字段(第 8.3.1.4 节)的写入发生在每个后续 读取该字段。

如果x = 1 在程序顺序中出现在bExit = true 之前,那么我们在它们之间就有了happens-before。如果其他线程在此之后读取bExit,那么我们在写入和读取之间发生了发生之前。由于传递性,我们在x = 1 和第二个线程读取bExit 之间也有happens-before。

5) 另外,如果我们有 volatile Person 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);

这里,getArraysetArrayarray 字段的普通 setter 和 getter。但是由于代码更改了数组的元素,因此有必要将数组的引用写回到它来自的位置,以便对数组元素的更改变得可见。请注意,即使被替换的元素与最初存在的元素相同,它也会完成!正是因为该元素的某些字段可能已被调用线程更改,因此有必要将这些更改传播给未来的读者。

6) 在 2 次后续读取 volatile 之前是否有任何情况发生 场地?我的意思是第二次阅读是否会看到线程的所有更改 它在它之前读取这个字段(当然我们只会有变化 如果 volatile 影响它之前所有变化的可见性 - 我就是 有点迷茫到底是真是假)?

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

【讨论】:

  • 很好的解释。我有几个问题,因为我不确定我是否正确理解了一些事情。 1) 你写了now flush whatever you've been doing there to the shared memory 关于同步块。但是我们只会看到我们在同步块中访问的变量或线程调用同步所做的所有更改(即使在同步块中未访问的变量上)?
  • 2) 在本章中,您在(JLS 的)帖子中写道:A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.。这是否意味着当变量是 volatile 时,您只会看到它的变化(因为它是写写发生在读之前,而不是发生在它们之间的每个操作之前!)。我的意思不是说,在问题描述中给出的示例中,我们可以看到 bExit = true,但如果只有 bExit 是 volatile,我们可以在第二个线程中看到 x = 0?
  • 2 - 继续)我问,因为我在这里找到了这个问题:http://java67.blogspot.bg/2012/09/top-10-tricky-java-interview-questions-answers.html 并且写到如果 bExit 是 volatile 程序是好的。所以寄存器只会刷新 bExits 值还是 bExits 和 x 值?
  • 3) 为什么在使用同步块时监控有关内存可见性的事项?我的意思是当尝试退出同步块时,不是所有变量(我们在这个块中访问的或线程中的所有变量 - 这与第一个问题有关)都从寄存器刷新到主内存或广播到所有 CPU 缓存?为什么同步对象很重要?我只是无法想象什么是关系以及它们是如何产生的(在同步对象和内存之间)。
  • 3-continue) 我知道我们应该使用同一个监视器来查看这些变化,但我不明白应该可见的内存是如何映射到对象的。抱歉,问题很长,但这些问题对我来说真的很有趣,并且与问题相关(我会为这个入门发布问题)。
【解决方案2】:

这取决于决定线程是否将变量的副本保存在自己的内存中的实现。在类级别变量的情况下,线程具有共享访问权限,而在局部变量的情况下,线程将保留一份副本。我将提供两个例子来说明这一事实,请看一下。

在您的示例中,如果我理解正确,您的代码应该看起来像这样--

package com.practice.multithreading;

public class LocalStaticVariableInThread {
    static int x=0;
    static boolean bExit = false;

    public static void main(String[] args) {
        Thread t1=new Thread(run1);
        Thread t2=new Thread(run2);

        t1.start();
        t2.start();

    }

    static Runnable run1=()->{
        x = 1; 
        bExit = true;
    };

    static Runnable run2=()->{
        if (bExit == true) 
            System.out.println("x=" + x);
    };

}

输出

x=1

总是得到这个输出。这是因为线程共享变量,并且当它被一个线程更改时,其他线程可以看到它。但在现实生活场景中,我们永远不能说哪个线程会先启动,因为这里的线程没有做任何事情,我们可以看到预期的结果。

现在举这个例子—— 在这里,如果您将 for-loop 中的 i 变量作为静态变量,那么线程将不会保留它的副本,您也不会看到所需的输出,即即使您已同步,每次计数值也不会是 2000计数增量。

package com.practice.multithreading;

public class RaceCondition2Fixed {
    private  int count;
    int i;

    /*making it synchronized forces the thread to acquire an intrinsic lock on the method, and another thread
    cannot access it until this lock is released after the method is completed. */
public synchronized void increment() {
    count++;
}
    public static void main(String[] args) {
        RaceCondition2Fixed rc= new RaceCondition2Fixed();
        rc.doWork();
    }

    private void doWork() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for ( i = 0; i < 1000; i++) {
                    increment();
                }
            }

        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for ( i = 0; i < 1000; i++) {
                    increment();
                }
            }

        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        /*if we don t use join then count will be 0. Because when we call t1.start() and t2.start() 
        the threads will start updating count in the spearate threads, meanwhile the main thread will
        print the value as 0. So. we need to wait for the threads to complete. */
        System.out.println(Thread.currentThread().getName()+" Count is : "+count);
    }

}

【讨论】:

    猜你喜欢
    • 2011-10-22
    • 2020-07-17
    相关资源
    最近更新 更多