0

我在互联网上遇到了以下 Java 类:

public class Lock1 implements Runnable {
int b=100;
public synchronized void m1() throws Exception {
    b=1000;
    Thread.sleep(50);
    System.out.println("b="+b);
}

public synchronized void m2() throws Exception {
    Thread.sleep(30);
    //System.out.println("m2");
    b=2000;
}

public void run() {
    try {m1();}
    catch(Exception e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) throws Exception {
    Lock1 tt=new Lock1();
    Thread t = new Thread(tt);
    t.start();

    tt.m2();
    System.out.println(tt.b);
}
}

尝试运行了很多次,结果几乎总是:

     1000
     b=1000

在我最初的猜测中,我认为第一行应该是“2000”,因为 tt.m2() 只是一个方法调用(不是线程),所以 main 方法应该继续执行并得到结果“b”作为一个在方法 m2 中被赋值为 2000。

我所做的第二次尝试是取消注释

 System.out.println("m2") 

在 m2 方法中。令人惊讶的是,结果几乎总是:

 m2
 2000
 b=1000

为什么在 m2 方法中添加语句,会导致 tt.b 的输出值发生变化?

对不起,我在这里对线程和方法调用之间的区别感到很困惑,希望高手能帮忙!

4

3 回答 3

5

Java 意义上的同步结合了几件事。在这种情况下,这些点很有趣:

  • 互斥
  • 读者的记忆障碍
  • 作家的记忆障碍

输入synchronized块(或方法)后,您有两个保证:您拥有锁(互斥),并且 JVM 和编译器将丢弃 同步对象的任何缓存。这意味着访问this.b将从 RAM 中获取 'b' 的实际值,而不是从任何缓存中获取,而只会从一次中获取。然后它将再次使用缓存的副本。

依次留下一个synchronized块可以保证 CPU 将所有脏的(即写入的)缓存刷新到内存中。

你的东西的重点是:System.out.println(tt.b);绝不是同步的,这意味着对它的访问没有跨越定义的内存屏障。因此,尽管另一个线程已经写入了一个新值b并将其刷新到 RAM 中,但主线程并不知道它应该b从 RAM 中读取,而不是从自己的缓存中读取。

解决方案是:

synchronized(tt){
    System.out.println(tt.b);
}

这符合黄金法则,如果某些东西是同步的,那么对它的每次访问都应该同步,而不仅仅是一半的访问。

关于你添加的System.out:有三件事:

首先:它很慢(与一些内存摆弄相比)。这意味着在此期间 CPU 或 JVM 可能会自行决定,新的外观tt可能是合适的

第二:它很大(与一些内存摆弄相比)。这意味着仅被触摸的代码可能会tt从缓存中逐出。

第三:内部同步。这意味着您跨越了一些记忆障碍(这可能与您无关tt- 谁知道)。但这些也可能会产生一些影响。

这是多线程调试的主要规则:System.out根据 Murphy 的说法,添加以捕获错误实际上会隐藏问题。

于 2013-04-06T15:26:21.120 回答
1

我想这是特定于 JVM 实现的。

基本上,每个线程都有自己的对象变量副本(视图),并且不确定它们来回同步的方式。

于 2013-04-06T15:03:17.230 回答
1

最可能的原因System.out.println是速度慢。“意外”结果的原因是延迟 ( Thread.sleep) 和打开输出流 ( System.out.println) 的开销之间的竞争条件。

于 2013-04-06T15:20:52.060 回答