Java更多的库谜题76:乒乓
下面的程序都是由同步的静态方法组成的。那么它会打印出什么呢?每次运行这个程序,能保证打印出同样的内容吗?
public class ping pong {
public static synchronized void main(String[]a){
Thread t = new Thread(){
public void run(){ pong();}
};
t . run();
system . out . print(" Ping ");
}
静态同步void Pong(){
system . out . print(" Pong ");
}
}
在多线程程序中,通常正确的观点是程序每次运行的结果都可能发生变化,但上面的程序总是打印相同的内容。在一个同步的静态方法被执行之前,它将获得一个与其类对象相关联的监控锁[8.4.3.6·JLS]。所以在上面的程序中,主线程将在创建第二个线程之前获得与PingPong.class关联的锁。只要主线程持有这个锁,第二个线程就不可能执行同步的静态方法。具体来说,第二个线程只能在打印出main方法的Ping并完成执行后才能执行pong方法。只有当主线程放弃锁时,才允许第二个线程获取锁并打印Pong。根据上面的分析,我们似乎确信这个程序应该总是打印PingPong。但是这里有一个小问题:当你尝试运行这个程序时,你会发现它总是打印PongPing。到底发生了什么?
虽然看起来很奇怪,但这个程序不是多线程程序。不是多线程程序?不会这样吧肯定会产生第二个线程。哦,是的,它确实创建了第二个线程,但它从未启动这个线程。相反,主线程将调用新线程实例的run方法,该方法将在主线程中同步运行。因为一个线程可以重复获取同一个锁[JLS 17.1],所以当run方法调用pong方法时,允许主线程再次获取与PingPong.class关联的锁。Pong方法打印Pong并返回到run方法,run方法又返回到main方法。最后,main方法打印Ping,解释我们看到的输出结果是如何来的。
要修改这个程序,很简单,只需要把t.run重写为t.start这样做了之后,这个程序就会一直按照你的意愿打印出PingPong。
这个教训很简单:当你想调用一个线程的start方法时要小心,不要犯调用这个线程的run方法的错误。可惜这个错误太常见了,可能很难发现。或许这个谜题的教训应该是针对API设计者的:如果一个线程没有一个公共的run方法,那么程序员就不可能不小心调用它。Thread类之所以有公共run方法,是因为它实现了Runnable接口,但这种方式并不是必须的。另一个可选的设计方案是使用组合而不是接口继承,这样每个线程实例都封装了一个Runnable。正如谜题47中所讨论的,组合通常比继承更好。这个难题表明,上述原则甚至适用于接口继承。
0条评论