繁体   English   中英

synchronized 关键字如何在内部工作

[英]how synchronized keyword works internally

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

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 个线程将共享主内存中的变量。 这是有道理的,但在同步的情况下,它将如何解决,因为两个线程都有自己的变量副本。

请帮我。

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

缓存

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

重新排序

这个就更玄了。 这个

x = 1; 
bExit = true;

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

同步进来

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

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

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

易失性变量

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

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

问答

1)您写了关于同步块的“现在将您在那里所做的任何事情都刷新到共享内存中”。 但是我们只会看到我们在同步块中访问的变量或线程调用同步所做的所有更改(甚至在同步块中未访问的变量上)?

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

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

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

2)在本章中,您(JLS 的)发布了这样的内容:“对易失性字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。” 这是否意味着当变量是 volatile 时,您只会看到它的变化(因为它是写写发生在读之前,而不是在它们之间的每个操作之前发生!)。 我的意思是说,这是否意味着在问题描述中给出的示例中,我们可以看到 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 中的推理相同,如果x = 1之后执行bExit = true ,那么由于程序顺序,存在线程内发生在之前的关系。 现在,由于 volatile 写入发生在 volatile 读取之前,因此可以保证第二个线程将看到第一个线程在将true写入bExit之前更新的任何bExit 请注意,此行为仅从 Java 1.5 左右开始,因此较旧或有缺陷的实现可能支持也可能不支持。 我在标准 Oracle 实现中看到过使用此功能的位(java.concurrent 集合),因此您至少可以假设它在那里工作。

3) 为什么在使用关于内存可见性的同步块时监控很重要? 我的意思是,当尝试退出同步块时,不是所有变量(我们在这个块中访问的或线程中的所有变量 - 这与第一个问题有关)都从寄存器刷新到主内存或广播到所有 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)。

对 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) 时,我们是否有一些依赖性,或者在这种情况下我们有内存障碍(假设年龄不是 volatile)? - 我觉得不行

你是对的。 由于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 影响其之前所有更改的可见性时,我们才会进行更改 - 无论是否属实,我都有些困惑)?

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

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

在你的例子中,如果我理解正确的话,你的代码应该是这样的——

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);
    }

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM