有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

多线程Java线程乒乓示例

我试图理解线程的基本知识,作为第一个例子,我创建了两个线程,它们在标准输出上写一个字符串。据我所知,调度程序允许使用循环调度执行线程。这就是为什么我得到:

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

有人能解释一下我错过了什么,为什么我没有打乒乓球。。。乒乓球。 谢谢


共 (6) 个答案

  1. # 1 楼答案

    在与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时间(和精力!)。正确的方法是使用阻塞方法,使线程处于休眠状态,直到收到相关事件的通知

  2. # 2 楼答案

    我的解决方案是:

    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();
            }
    
    
    
    
    
    
    
    
    }
    }
    
  3. # 3 楼答案

    这是一个用Java编写的乒乓球程序。乒乓球和乒乓球是分开的。每个线程既是消费者又是生产者。当每个线程运行时,它做两件事

    1. 生成一条允许另一方(作为消费者)运行的消息
    2. 使用导致自身挂起的消息

    代码基于Oracles ProducerConsumereSample。请注意,Ping和Pong类的代码和行为几乎相同。 OP代码中的线程只使用对象监视器的“互斥”部分(正如Brian Agnew在上面所建议的)。他们从不要求等待。因此,它们只相互排斥,而从不调用java运行时来允许另一个线程运行

    /*
     * Copyright (c) 1995, 2008, Oracle and/or its affiliates. All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     *
     *   - Redistributions of source code must retain the above copyright
     *     notice, this list of conditions and the following disclaimer.
     *
     *   - Redistributions in binary form must reproduce the above copyright
     *     notice, this list of conditions and the following disclaimer in the
     *     documentation and/or other materials provided with the distribution.
     *
     *   - Neither the name of Oracle or the names of its
     *     contributors may be used to endorse or promote products derived
     *     from this software without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    
     * based on oracle example on sync-wait-notify
     * cf. https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
     * run with java ProducerConsumerExample
     * 
     *
     */ 
    
    public class ProducerConsumerExample {
        public static void main(String[] args) {
            Drop drop = new Drop();
        DropCtoP dropCtoP = new DropCtoP();
        (new Thread(new Ping(drop,dropCtoP))).start();
            (new Thread(new Pong(drop,dropCtoP))).start();
        }
    }
    
    
    public class Pong implements Runnable {
        private Drop drop;
        private DropCtoP dropCtoP;
        private int count=0;
    
        public Pong(Drop drop,DropCtoP dropCtoP) {
            this.drop = drop;
            this.dropCtoP = dropCtoP;
        }
    
        public void run() {
            String message;
            for (;;) {
            count++;
                message = drop.take();
                System.out.format("Pong running - : %s - ran num times %d %n", message,count);
                dropCtoP.put("Run ping token");
            }
        }
    }
    
    
    
    public class Ping implements Runnable {
        private Drop drop;
        private DropCtoP dropCtoP;
        private int count=0;
    
        public Ping(Drop drop,DropCtoP dropCtoP) {
            this.drop = drop;
            this.dropCtoP = dropCtoP;
        }
    
        public void run() {
    
            String message;
            for (;;) {
          count++;
          drop.put("Run pong token");
          message = dropCtoP.take();
          System.out.format("PING running - : %s- ran num times %d %n", message,count);
            }
    
        }
    }
    
    
    
    public class DropCtoP {
        // Message sent from producer
        // to consumer.
        private String message;
        // True if consumer should wait
        // for producer to send message,
        // false if producer should wait for
        // consumer to retrieve message.
        private boolean empty2 = true;
    
    
        public synchronized String take() {
            // Wait until message is
            // available.
            while (empty2) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty2 = true;
            // Notify producer that
            // status has changed.
            notifyAll();
            return message;
        }
    
        public synchronized void put(String message) {
            // Wait until message has
            // been retrieved.
            while (!empty2) {
                try { 
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty2 = false;
            // Store message.
            this.message = message;
            // Notify consumer that status
            // has changed.
            notifyAll();
        }    
    }
    
    
    public class Drop {
        // Message sent from producer
        // to consumer.
        private String message;
        // True if consumer should wait
        // for producer to send message,
        // false if producer should wait for
        // consumer to retrieve message.
        private boolean empty = true;
    
        public synchronized String take() {
            // Wait until message is
            // available.
            while (empty) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty = true;
            // Notify producer that
            // status has changed.
            notifyAll();
            return message;
        }
    
        public synchronized void put(String message) {
            // Wait until message has
            // been retrieved.
            while (!empty) {
                try { 
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty = false;
            // Store message.
            this.message = message;
            // Notify consumer that status
            // has changed.
            notifyAll();
        }
    
    
    }
    
  4. # 4 楼答案

    一种选择是使用SynchronousQueue

    import java.util.concurrent.SynchronousQueue;
    
    public class PingPongPattern {
    
        private SynchronousQueue<Integer> q = new SynchronousQueue<Integer>();
        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();
        }
    }
    
  5. # 5 楼答案

    这一行:

    public synchronized void playTurn(){
        //code
    }
    

    在行为上等同于

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

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

    如果要使用turn变量进行同步,例如:

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

    然后情况会好得多(运行多次以验证),但也没有100%的同步。在乞讨中(大部分情况下),你会得到一个双乒乓球和双乒乓球,之后它们看起来是同步的,但你仍然可以得到双乒乓球/乒乓球

    同步块锁定(参见此great answer),而不是对该值的引用。(见编辑)

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

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

    来验证我是否试着

    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;
    

    因此,线程总是可以在同一个值上进行同步——实际上不是:)正如上面给出的great answer中所解释的那样——字符串(不可变对象)与锁一样危险——因为如果在程序中的100个位置上创建字符串“A”,那么所有100个引用(变量)都将指向内存中相同的“A”——因此可以过同步

    所以,要回答最初的问题,请修改代码如下:

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

    并行乒乓球示例将100%正确实现(参见编辑^2)

    上述代码相当于:

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

    乒乓球线。类是一个Class object,例如,在每个实例上,您都可以调用getClass(),它总是只有一个实例

    你也可以这样做

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

    此外,阅读和编程示例(必要时多次运行)这tutorial

    编辑:

    technically correct

    synchronized方法与在此基础上锁定synchronized语句相同。让我们将synchronized语句的参数称为“lock”——正如Marko指出的,“lock”是一个变量,用于存储对类的对象/实例的引用。引用规范:

    The synchronized statement computes a reference to an object; it then attempts to perform a lock action on that object's monitor..

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

    Each object in Java is associated with a monitor..

    效果不变

    编辑^2:

    就评论意见采取后续行动: “并行乒乓球示例将100%正确实现”——这意味着实现了预期的行为(没有错误)

    如果结果是正确的,那么解决方案就是正确的。解决问题的方法有很多种,因此下一个标准是解决方案的简单性/优雅性——相量解决方案是更好的方法,因为正如Marko在一些评论中所说的,使用phaser对象比使用同步机制出错的可能性小得多——这可以从本文中所有(非)解决方案变体中看出。值得注意的是代码大小和整体清晰度的比较

    总之,只要适用于所讨论的问题,就应该使用这个sort of constructs

  6. # 6 楼答案

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

    然而,我认为这是行不通的。我认为您应该查看wait()notify()来实现这一点(如果您想了解线程原语)。有关示例,请参见this