【问题标题】:Two threads, same static variable, same value, concurrent access两个线程,同一个静态变量,同一个值,并发访问
【发布时间】:2013-07-09 08:13:28
【问题描述】:


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

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 的规则是什么。

【问题讨论】:

  • R S R S 绝对不可能,因为如果 id==0 会触发“R S”,它肯定不会自行到达那里。不过,“P Q P Q”对我来说似乎是可能的——两个线程都看到 id==1(run() 没有同步),都调用 pick() ....
  • R S R S 在它们都运行时是可能的,将 id 递减为 -1 然后输入条件。但极不可能,因为第一个线程必须在递减后中断。
  • PQPQ 是不可能的,因为只有一个线程可以到达第 7 行的 if 语句,id 值为 0。原因是两个线程必须通过第 6 行,因此,如果留下一个线程,到达第 7 行时它的值为 -1。如果两个线程一起移动,那么到达第 7 行时它们的 id 值为 -1。
  • @Tsikon - 不。不要仅仅因为您将其视为 java 的一行而认为它是原子的。您可以将“id = 1 - id”视为“int temp = id-1; id=temp;”。
  • @Tsikon 你是如何真正找到1-id! 的 -1 值的?你怎么确定只有一个线程在第 7 行?

标签: java multithreading concurrency static


【解决方案1】:

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

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

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

  • 1

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

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

    有可能输出是P Q P Q

  • 0

    它从线程 1 中看到翻转的值

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

    选项A、B、C的情况

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

【讨论】:

    【解决方案2】:

    实际上,有很多可能性,有些极不可能,但它们仍然是可能的,经过 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 表示出色的工作。百万分之一是巧合(没有统计学意义),您无法根据它确定 PQPQ 概率。虽然它确实证明了这种组合是可能的。
    • 这是一个很好的答案,先生,我很感激。这证明了OCP Java SE 6 程序员实践考试这本书有一个缺陷。无论如何都要感谢贝茨和塞拉。 (:
    • 我觉得test有问题!在原始代码中,两个方法可以一起工作,因为第一个方法持有类锁,第二个持有对象锁,但在你的测试中,它们都需要一个用于字符串缓冲区的锁,所以这不是一个公平的测试:)
    • @3yanlis1bos 这也不是第一次 :) 祝你考试顺利,无论如何(实际上比书本更容易......)
    • @vikingsteve 哦,很高兴听到这个消息。有些问题让我很难过,但我可以肯定地说我学到了很多。谢谢你的美好祝愿。 :)
    【解决方案3】:

    是的,你是对的,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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        你的假设是正确的。 P Q P Q 确实是可能的,因为JLS Specification 17.4.3 声明如下:

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

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

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

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

        AtomicInteger's 会更好地避免这种情况。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-10-20
          • 2012-03-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-07-07
          相关资源
          最近更新 更多