我一直在努力为下周必须参加的 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-}
答案的关键是:
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的规则是什么。
答案 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之前启动。