两个线程,相同的静态变量,相同的值,并发访问

时间:2013-07-11 06:56:05

标签: java multithreading concurrency static


我一直在努力为下周必须参加的 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-}
  • 哪些是真的? (选择所有适用的选项。)
  • 输出可以是P Q R S
  • 输出可以是P R S Q
  • 输出可以是P R Q S
  • 输出可以是P Q P Q
  • 该程序可能会导致死锁。
  • 编译失败。

答案的关键是:
A,B和C是正确的。由于pick()是静态的,而release()是非静态的,因此 是两把锁。如果pick()是非静态的,那么只有A才是正确的。

它还说输出 P Q P Q实际上不是一个选项,并且不可能得到这样的结果。

一开始,我真的不相信答案的关键,但后来我发现由于这个应用程序,我们真的不可能看到这个输出。 (运行课程后。)

现在,这是让我感到困惑的一部分,这就是为什么

我认为必须有P Q P Q或R S R S结果。因为总是有可能使两个线程的变量id完全相同。换句话说,例如,当第一个线程刚刚完成执行第6行时,它可以放弃转向另一个,之后,另一个可以更改变量id的值,然后瞧!他们可以快乐地进入同一个区域

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

5 个答案:

答案 0 :(得分:6)

实际上,有很多可能性,有些是极不可能的,但它们仍然是可能的,并且在执行了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

答案 1 :(得分: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

答案 2 :(得分:1)

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

答案 3 :(得分:1)

你的假设是正确的。 P Q P Q确实可行,因为JLS Specification 17.4.3表明了以下内容:

  

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

     

如果所有操作都以与程序顺序一致的总顺序(执行顺序)发生,则一组操作顺序一致,此外,变量v的每个读取r都将写入写入的值v视为v这样:

     
      
  • w在执行顺序中出现在r之前,
  •   
  • 没有其他写w',因此w在w'之前出现,w'在执行顺序中出现在r之前。
  •   
     

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

AtomicInteger是避免这种情况的更好的候选人。

答案 4 :(得分:0)

  

当第一个线程刚刚完成执行第6行时,它可以放弃转向另一个,之后,另一个可以更改变量id的值,然后瞧!他们可以快乐地进入相同的区域。

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

线程2启动。 它要么看到id

的值
  • <强> 1

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

    线程2启动。它将值翻转为0.并且它们都进入第一个if块。 线程1是否已在第7行暂停。结果可能有所不同。

    输出可能是P Q P Q

  • 0

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

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

    选项A,B,C

  • 的情况

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