线程和静态变量

3

我试图理解这段代码。当我运行此代码时,输出将为 Roger。msg不是一个静态变量,并且在类级别上应该打印Moore,对吗?

编辑:我允许了一段时间的睡眠,以便子线程完成其工作。它还打印printing..,但仍然没有改变。

public class Test2 {
    private static String msg = "Roger";

    static {
        new Thread(new Runnable() {
            public void run() {
                System.out.println("printing.."); 
                msg += "Moore";
            }
        }).start();
    }

    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }

    public static void main(String argv[]) {
        System.out.println(msg); 
    }
}

1
看起来像是一个简单的竞态条件。我想知道如果在main()函数中你睡眠一秒钟,它是否会输出“Moore”而不是“Roger”? - CmdrMoozy
是的 - 就是这样。增加一个 Thread.sleep 以延长持续时间确实打印了相关输出!感谢您的答案!请将您的答案添加到答案中,以便我可以“回答”它...或者我可以回答 papmplhet - 两者都是正确的... - user2796381
4个回答

5
尝试理解这段代码。运行时输出将是Roger。msg不是静态变量且在类级别,因此应该打印Moore,这让我困惑。正如其他人指出的那样,这是一种竞争条件,但它比这个简单的答案更复杂。编辑:我允许睡眠以便子线程完成其任务。它还打印printing...仍然没有改变。
当类被初始化时,static 代码会在首次访问该类的线程中执行,即主线程。所有其他线程都必须等待此初始化完成,然后才能访问该类。这意味着后台线程实际上会停止并等待类初始化完成,然后才能执行 msg += "Moore";。然后就成了一场比赛,看看消息是否被分配为 "Roger" 并且后台线程可以在 main 打印它之前追加它。即使 msg 字段是 volatile 的,竞争仍然存在。您可以从 JLS section 12.4.2 on Detailed Initialization Procedure 中了解到该过程的复杂性。

因此,大致上发生了以下情况:

  1. 主线程初始化Test2类。
  2. msg首先被初始化,因为它在static块之前。
  3. 首先执行第一个static块,该块分叉后台线程。
  4. 执行第二个static块,该块使用sleep()阻塞初始化线程。
  5. 后台线程开始运行(可能在上一步之前)。 它尝试更新msg,但由于主线程正在睡眠并且尚未完成类的初始化,因此该类被锁定。 后台线程必须等待。
  6. 主线程醒来并完成初始化。
  7. 这将释放对类的锁定,从而允许后台线程继续。
  8. 与上一步同时进行,调用main,竞争条件是看是否可以在打印出之前更新msg

通常,在static方法中分叉后台线程是极不推荐的。 在static块中放置sleep显然也不推荐。


+1:有趣。JSL中是否有关于正在发生的内容的章节? - Martijn Courteaux
我已经阅读了那一部分几分钟,但我认为这更多关于谁(即哪个线程)正在初始化类,而不是真正防止访问类成员。不过,我会重新阅读的。 - Martijn Courteaux
事实上,如果您严格解释有关静态类初始化程序的所有规则,则§12.4.2是对此行为的完美解释。因为第二个线程也尝试初始化该类,因为当它尝试写入msg时,它会发现类加载尚未完成,但是根据该部分的描述,这是不允许的。它声明:如果C的Class对象指示另一个线程正在进行C的初始化,则释放LC并阻止当前线程,直到收到通知,表明正在进行的初始化已完成,然后重复此步骤。 - Martijn Courteaux
嗯,是的。12.4.2解释了这个问题。这不就是我的答案所说的吗?实际上,是msg的_read_导致了后台线程阻塞@MartijnCourteaux。 - Gray
是的,完全正确。但是你在第5步的回答有点含糊不清。第二个线程再次启动静态初始化过程,因为当它尝试读取msg时,该类尚未初始化(因为主线程正在初始化该类时处于睡眠状态)。在init过程中,有一个并发检查以确保没有其他线程正在初始化该类,这在本例中是成立的。这就是§12.4.2第2步中描述的内容。 - Martijn Courteaux
显示剩余3条评论

2

这是一种竞争条件。无法保证 Runnable 何时执行。

编辑:这个答案回应了最初发布的问题,在其中静态初始化程序中没有延迟。这导致主线程读取静态成员和生成的线程更新它之间存在简单的竞争条件。


1
这对我来说似乎不是竞态条件。睡眠一秒钟应该给另一个线程足够的时间来完成。请查看此示例:http://ideone.com/DVMZ0f - Martijn Courteaux
@MartijnCourteaux,OP已确认延迟主线程会改变输出。 - pamphlet
如果您在主方法中延迟线程,静态初始化程序将会完成。这样,第二个线程就可以开始工作了。在他的代码示例中,他正在静态初始化程序中使用睡眠功能。这意味着当另一个线程尝试(实际上等待)更改静态变量“msg”时,类初始化仍未完成。 - Martijn Courteaux
自从我的答案写出来以后,他的代码已经改变了。原始问题中提供的代码没有延迟。建议是将延迟放在main()中(就像您上面的建议一样)。我强烈坚持认为,对于OP的原始问题,答案是这是一种竞争条件。 - pamphlet
让我们在聊天室中继续这个讨论 - pamphlet
显示剩余2条评论

2

在你的类中所有静态初始化器都完成之前,主方法不会被调用。因此,它将一直等待静态初始化完成。即使其中有一个休眠。

此外,静态初始化是线程安全的,因此在静态初始化块完成之前,您的分支线程无法访问变量。


0

与其等待一段时间,希望其他线程运行,不如通过一些同步来保证它:

public class Test {
    private static String msg = "Roger";
    private static volatile boolean done = false;
    private static final Object lock = new Object();
    static {
        new Thread(new Runnable() {
            public void run() {
                synchronized(lock)
                {
                    lock.notify();
                    System.out.println("printing.."); 
                    msg += "Moore";
                    done=true;
                }
            }
        }).start();
    }

    public static void main(String argv[]) {
        synchronized(lock)
        {
            while(!done)
            {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        System.out.println(msg);
    }
}

如果主线程先获得锁,则它将执行msg.wait。它将一直等待,直到调用notify(实际上是在包含notify的同步块完成时继续执行)。如果新线程先获得锁,则主线程将不得不在其同步块的开头等待。一旦进入,done将为true。它不会等待,而是直接通过。

这个方案有几个问题。如果done在多个线程中被更新,那么它应该是volatile的。但真正的问题是你试图在一个引用正在改变的对象上进行同步。这是行不通的。 - Gray
如果可能的话,锁对象应该始终是final。否则这看起来不错。 - Gray

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接