Java Thread Ping Pong示例

Kum*_*mmo 3 java concurrency multithreading

我试图理解线程基础知识,作为第一个例子,我创建了两个在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);
    }
}
}
Run Code Online (Sandbox Code Playgroud)

主要课程:

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

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

PING PING pong pong pong PING PING PING pong pong

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

Mar*_*nik 12

在我与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();
}
Run Code Online (Sandbox Code Playgroud)

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


lin*_*ski 10

这一行:

public synchronized void playTurn(){
    //code
}
Run Code Online (Sandbox Code Playgroud)

等同于行为

public void playTurn() {
    synchronized(this) {
         //code
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

private static String turn = ""; // must initialize or you ll get an NPE

public void playTurn() {
    synchronized(turn) {
         //...
         turn = msg; // (1)
         //...
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

thread1 locks on ""
thread2 blocks on ""
thread1 changes the value of turn variable to "PING" - thread2 can continue since "" is no longer locked 
Run Code Online (Sandbox Code Playgroud)

验证我是否尝试过

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

之前和之后

turn = msg;
Run Code Online (Sandbox Code Playgroud)

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

 try {
    Thread.yield();
    Thread.currentThread().sleep(1000); //  also  try multiple times
 } 
 catch (InterruptedException ex) {}
Run Code Online (Sandbox Code Playgroud)

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

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

    turn = msg;
Run Code Online (Sandbox Code Playgroud)

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

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

 public void playTurn() {
    synchronized(PingPongThread.class) {
         //code
    }
}
Run Code Online (Sandbox Code Playgroud)

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

上面的代码相当于:

 public static synchronized void playTurn() {
     //code
 }
Run Code Online (Sandbox Code Playgroud)

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

你也可以这样做

 public static Object lock = new Object();

 public void playTurn() {
    synchronized(lock) {
         //code
    }
}
Run Code Online (Sandbox Code Playgroud)

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

编辑:

技术上讲是正确的:

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

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

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

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

效果保持不变.

编辑^ 2:

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

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

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