Java Thread Ping Pong示例

我试图理解线程基础知识,作为第一个例子,我创建了两个在stdout上写一个String的线程。 据我所知,调度程序允许使用循环调度执行线程。 这就是我得到的原因:

PING PING pong pong pong PING PING PING pong pong

现在我想使用一个共享变量,所以每个线程都会知道你的轮到你了:

public class PingPongThread extends Thread { private String msg; private static String turn; public PingPongThread(String msg){ this.msg = msg; } @Override public void run() { while(true) { playTurn(); } } public synchronized void playTurn(){ if (!msg.equals(turn)){ turn=msg; System.out.println(msg); } } } 

主要课程:

 public class ThreadTest { public static void main(String[] args) { PingPongThread thread1 = new PingPongThread("PING"); PingPongThread thread2 = new PingPongThread("pong"); thread1.start(); thread2.start(); } } 

我同步了“转经理”,但我仍然得到类似的东西:

PING PING pong pong pong PING PING PING pong pong

有人可以解释我错过了什么,为什么我没有得到乒乓球……乒乓球 。 谢谢!

这一行:

 public synchronized void playTurn(){ //code } 

等同于行为

 public void playTurn() { synchronized(this) { //code } } 

这就是为什么没有发生同步的原因,因为正如Brian Agnew所指出的那样,线程正在同步两个不同的对象(thread1,thread2),每个对象都在它自己的实例上,导致没有有效的同步。

如果您使用转弯变量进行同步,例如:

 private static String turn = ""; // must initialize or you ll get an NPE public void playTurn() { synchronized(turn) { //... turn = msg; // (1) //... } } 

然后情况要好得多(运行多次validation),但也没有100%的同步。 在开始(大多数情况下),你得到一个双击和双乒乓球,然后他们看起来同步,但你仍然可以得到双ping / pongs。

synchronized块会锁定 (请参阅此大答案 ),而不是对该值引用 。 (见编辑)

那么让我们来看一个可能的场景:

 thread1 locks on "" thread2 blocks on "" thread1 changes the value of turn variable to "PING" - thread2 can continue since "" is no longer locked 

validation我是否尝试过

 try { Thread.currentThread().sleep(1000); // try with 10, 100 also multiple times } catch (InterruptedException ex) {} 

之前和之后

 turn = msg; 

它看起来是同步的吗?! 但是,如果你把

  try { Thread.yield(); Thread.currentThread().sleep(1000); // also try multiple times } catch (InterruptedException ex) {} 

几秒钟后,你会看到双击/双击。 Thread.yield()本质上意味着“我已经完成了处理器,让其他线程工作”。 这显然是我操作系统上的系统线程调度程序实现。

因此,要正确同步,我们必须删除行

  turn = msg; 

所以线程总是可以在同一个值上同步 – 不是真的:)正如上面给出的很好的答案所解释的 – 字符串(不可变对象)作为锁是危险的 – 因为如果你在程序中的100个地方创建字符串“A”全部100引用(变量)将指向内存中的相同“A” – 因此您可能会过度同步

因此,要回答您的原始问题,请修改您的代码,如下所示:

  public void playTurn() { synchronized(PingPongThread.class) { //code } } 

并行PingPong示例将100%正确实现(请参阅编辑^ 2)。

以上代码相当于:

  public static synchronized void playTurn() { //code } 

PingPongThread.class是一个Class对象 ,例如,在每个可以调用getClass()的实例上,它总是只有一个实例。

你也可以这样做

  public static Object lock = new Object(); public void playTurn() { synchronized(lock) { //code } } 

此外,阅读和编​​程示例(必要时运行多次)本教程 。

编辑:

从技术上讲是正确的 :

synchronized方法与此处的synchronized语句锁定相同。 让我们调用synchronized语句“lock”的参数 – 正如Marko指出的那样,“lock”是一个存储对类的对象/实例的引用的变量。 引用规范:

synchronized语句计算对象的引用; 然后它尝试在该对象的监视器上执行锁定操作。

因此,同步不是在值上实现的 – 对象/类实例,而是在与该实例/值关联对象监视器上 。 因为

Java中的每个对象都与一个监视器相关联。

效果保持不变。

编辑^ 2:

继续评论备注:“并行PingPong示例将100%正确实现” – 意味着,实现了所需的行为(没有错误)。

恕我直言,如果结果是正确的,解决方案是正确的。 有很多方法可以解决这个问题,所以接下来的标准就是解决方案的简洁性和优雅性 – 移相器解决方案是更好的方法,因为正如Marko在某些评论中所说的那样使用移相器产生错误的可能性要小得多对象比使用同步机制 – 这可以从本文中的所有(非)解决方案变体中看出。 值得注意的还有代码大小和整体清晰度的比较。

总之,只要适用于有问题的问题,就应该使用这种结构 。

在我与Brian Agnew的讨论结束时,我提交了使用java.util.concurrent.Phaser来协调你的乒乓线程的代码:

 static final Phaser p = new Phaser(1); public static void main(String[] args) { t("ping"); t("pong"); } private static void t(final String msg) { new Thread() { public void run() { while (true) { System.out.println(msg); p.awaitAdvance(p.arrive()+1); } }}.start(); } 

此解决方案与您尝试编码的解决方案之间的主要区别在于您的解决方案忙于检查标志,从而浪费CPU时间(和能量!)。 正确的方法是使用阻塞方法使线程进入hibernate状态,直到通知相关事件。

PingPongThread每个实例都在自身进行同步,而不是在共享资源上进行同步。 为了控制传递的消息,你需要在共享资源上同步(例如你的turn变量?)

但是,我不认为这真的会起作用。 我认为您应该检查wait()notify()来执行此操作(如果您想了解线程原语)。 请参阅此示例。

我的解决方案是:

 public class InfinitePingPong extends Thread { private static final Object lock= new Object(); private String toPrintOut; public InfinitePingPong(String s){ this.toPrintOut = s; } public void run(){ while (true){ synchronized(lock){ System.out.println(this.toPrintOut +" -->"+this.getId()); lock.notifyAll(); try { lock.wait(); } catch (InterruptedException e) {} } } } public static void main(String[] args) throws InterruptedException { InfinitePingPong a = new InfinitePingPong("ping"); InfinitePingPong b = new InfinitePingPong("pong"); a.start(); b.start(); b.wait(); try { a.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 

一种选择是使用SynchronousQueue。

 import java.util.concurrent.SynchronousQueue; public class PingPongPattern { private SynchronousQueue q = new SynchronousQueue(); private Thread t1 = new Thread() { @Override public void run() { while (true) { // TODO Auto-generated method stub super.run(); try { System.out.println("Ping"); q.put(1); q.put(2); } catch (Exception e) { } } } }; private Thread t2 = new Thread() { @Override public void run() { while (true) { // TODO Auto-generated method stub super.run(); try { q.take(); System.out.println("Pong"); q.take(); } catch (Exception e) { } } } }; public static void main(String[] args) { // TODO Auto-generated method stub PingPongPattern p = new PingPongPattern(); p.t1.start(); p.t2.start(); } }