9


我一直在为下周必须参加的SCJP考试 做准备,我遇到了这个关于Java Threads的问题。

1-public class Stone implements Runnable {
2-  static int id = 1;
3-
4-  public void run() {
5-      try {
6-          id = 1 - id;
7-          if (id == 0) {
8-                      pick();
9-          } else {
10-             release();
11-         }
12-
13-     } catch (Exception e) {
14-     }
15- }
16-
17- private static synchronized void pick() throws Exception {
18-     System.out.print("P ");
19-     System.out.print("Q ");
20- }
21-
22- private synchronized void release() throws Exception {
23-     System.out.print("R ");
24-     System.out.print("S ");
25- }
26-
27- public static void main(String[] args) {
28-     Stone st = new Stone();
29-     new Thread(st).start();
30-     new Thread(st).start();
31- }
32-}
  • 哪些是真的?(选择所有适用的。)
  • 输出可能是 PQRS
  • 输出可以是 PRSQ
  • 输出可能是 PRQS
  • 输出可以是 PQPQ
  • 该程序可能会导致死锁。
  • 编译失败。

答案是:
A、B、C 正确。因为 pick() 是静态的,而 release() 是非静态的,所以有两个锁。如果 pick() 是非静态的,则只有 A 是正确的。

它还说输出PQPQ 不是一个真正的选项,并且不可能得到这样的结果。

一开始,我并不真的相信答案,但后来我发现,这个应用程序的结果真的不可能看到这个输出。(上课后。)

现在,这就是让我有点困惑的部分,这就是为什么

我认为 PQPQ 或 RSRS 结果一定是可能的。因为总是有机会使两个线程的变量 id 完全相同。换句话说,例如,当第一个线程刚刚执行完第 6 行时,它可以放弃轮到另一个线程,然后,另一个线程可以更改变量 id 的值,然后瞧!如果他们愉快地阻止,他们可以进入相同的

我试图一遍又一遍地看到这种情况(使用 Eclipse Juno 和 Java 7)。它只是不会发生。我确信我的思维方式有问题,我想知道它是什么。我需要知道阻止这两个线程以相同状态访问变量 id 的规则是什么。

4

5 回答 5

7

实际上,有很多可能性,有些极不可能,但它们仍然是可能的,经过 100 万次处决后,这就是我发现的。

代码:

public class Stone implements Runnable {
    static int id = 1;
    static StringBuffer buffer = new StringBuffer();

    public void run() {
        try {
            id = 1 - id;
            if (id == 0) {
                pick();
            } else {
                release();
            }

        } catch (Exception e) {
        }
    }

    private static synchronized void pick() throws Exception {
        buffer.append("P ");
        buffer.append("Q ");
    }

    private synchronized void release() throws Exception {
        buffer.append("R ");
        buffer.append("S ");
    }

    public static void main(String[] args) {
        int count = 1000000;
        Map<String, Integer> results = new HashMap<String, Integer>();
        System.out.println("Running " + count + " times...");
        for (int i = 0; i< count; i++) {
            buffer = new StringBuffer();
            Stone stone = new Stone();
            Thread t1 = new Thread(stone);
            Thread t2 = new Thread(stone);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
                // wait
            }
            String result = buffer.toString();
            Integer x = results.get(result);
            if (x == null) x = 0;
            results.put(result, x + 1);
            if (i > 0 && i % 50000 == 0) System.out.println(i + "... " + results.keySet());
        }
        System.out.println("done, results were:");
        for (String key : results.keySet()) {
            System.out.println(" " + key + ": " + results.get(key));
        }
    }
}

结果:

Running 1000000 times...
50000... [R S P Q , P Q R S , P R S Q , R P Q S ]
100000... [R S P Q , P Q R S , P R S Q , R P Q S ]
150000... [R S P Q , P Q R S , P R S Q , R P Q S ]
200000... [R S P Q , P Q R S , P R S Q , R P Q S ]
250000... [R S P Q , P Q R S , P R S Q , R P Q S ]
300000... [R S P Q , P Q R S , P R S Q , R P Q S ]
350000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
400000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
450000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
500000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
550000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
600000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
650000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
700000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
750000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
800000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
850000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
900000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
950000... [P Q P Q , R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
done, results were:
P Q P Q : 1
R S P Q : 60499
P Q R S : 939460
P R S Q : 23
P R Q S : 2
R P Q S : 15

我认为我们已经证明这P Q P Q确实是可能的,尽管概率极低,大约百万分之一......

[编辑:另一个运行,R S R S也可以显示不同的结果:]

done, results were:
 R S R S : 1
 R P S Q : 2
 P Q P Q : 1
 R S P Q : 445102
 P Q R S : 554877
 P R S Q : 5
 P R Q S : 2
 R P Q S : 10
于 2013-07-11T07:24:52.093 回答
1

是的,你是对的,P Q P Q是可能的。

您可以通过以下修改来增加此事件的概率(它不影响程序的语义):

public class Stone implements Runnable {
    static int id = 1;
    static CyclicBarrier b = new CyclicBarrier(2);

    public void run() {
        try {
            b.await(); // Increase probability of concurrent execution of subsequent actions

            int t = id;

            Thread.yield(); // Increase probability of thread switch at this point

            id = 1 - t;

            Thread.yield(); // Increase probability of thread switch at this point

            if (id == 0) {
                pick();
            } else {
                release();
            }
        } catch (Exception e) {}
    }
    ...
}

在应用了这些修改之后,我P Q P Q经过了几十次运行。

于 2013-07-11T07:25:41.973 回答
1

是的,你的怀疑是对的。但是,run()方法中的代码非常简单,可以在一次 CPU 突发中执行,除非通过其他方式等待。

于 2013-07-11T07:29:13.330 回答
1

你的假设是正确的。PQPQ 确实是可能的,因为JLS 规范 17.4.3规定了以下内容:

在每个线程 t 执行的所有线程间动作中,t 的程序顺序是一个总顺序,它反映了根据 t 的线程内语义执行这些动作的顺序。

如果所有动作都以与程序顺序一致的总顺序(执行顺序)发生,则一组动作是顺序一致的,此外,变量 v 的每次读取 r 都会看到写入 w 到 v 的值,这样:

  • w 在执行顺序中位于 r 之前,并且
  • 在执行顺序中,没有其他写入 w' 使得 w 在 w' 之前并且 w' 在 r 之前。

顺序一致性是对程序执行中的可见性和顺序的非常强大的保证。在顺序一致的执行中,所有单独的操作(例如读取和写入)都有一个与程序顺序一致的总顺序,并且每个单独的操作都是原子的,并且对每个线程都是立即可见的。

AtomicInteger将是避免这种情况的更好选择。

于 2013-07-11T07:48:40.627 回答
0

当第一个线程刚刚执行完第 6 行时,它可以放弃轮到另一个线程,然后,另一个线程可以更改变量 id 的值,然后瞧!如果愉快地阻塞,他们可以进入相同的位置。

假设线程 1 首先启动。它将 id 的值翻转为 0。线程 1 现在在第 8 行暂停。

线程 2 开始。它要么看到 id 的值

  • 1

    所有线程都可以在本地缓存字段,除非它们被标记为 volatile。线程 2 缓存 id 的值。

    线程 2 开始。它将值翻转为 0。它们都进入了第一个 if 块。如果线程 1 在第 7 行被挂起。结果可能会有所不同。

    输出可能是 PQPQ

  • 0

    它看到来自线程 1 的翻转值

    再次将值更改为 1。进入 else 块。

    选项 A、B、C 的情况

甚至不能保证线程 1 在线程 2 之前启动。

于 2013-07-11T07:17:35.057 回答