【问题标题】:Hello World with Multithreading Java使用多线程 Java 的 Hello World
【发布时间】:2015-08-30 10:18:52
【问题描述】:

我很想了解如何使用关键字:等待、通知/全部、同步,所以我决定尝试一个简单的示例。基本上我要做的是创建两个要打印字符串的线程。第一个线程有字符串“Hello”,而第二个线程有字符串“World”。

我想要达到的输出如下: 你好 世界 你好 世界 你好 世界 ...

这是我目前写的代码,但现在的输出是: 你好 你好 你好 ... 世界 世界 世界 ...

错误在哪里?谢谢你。 :)

代码如下:

class MyThread implements Runnable {
    private SimpleSyncThread sync;
    private String s;

    public MyThread(SimpleSyncThread sync, String s) {
        this.sync = sync;
        this.s = s;
    }

    public static void pause(long time) {
        try {Thread.sleep(time); }
        catch (InterruptedException e) {Thread.currentThread().interrupt();}
    }

    @Override
    public void run() {
        synchronized (sync) {
            for (int i = 0; i < 10; i++) {
                sync.print(s);
            }
        }
    }
}

public class SimpleSyncThread {

    public void print(String s) {
        System.out.println(s);
        MyThread.pause(200);
    }

    public static void main(String[] args) {
        SimpleSyncThread sync = new SimpleSyncThread();
        MyThread st1 = new MyThread(sync, "Hello");
        MyThread st2 = new MyThread(sync, "World");

        Thread t1 = new Thread(st1);
        Thread t2 = new Thread(st2);

        t1.start();
        t2.start();
    }
}

【问题讨论】:

  • "哪里是错误/s?"为什么你认为这段代码应该打印“hello world hello world ...”,你为什么这么认为?
  • 整个for循环都在同步块中。这就是为什么你得到所有的问候,然后是所有的世界
  • 那么应该怎么写同步块呢?
  • 你可以将它移动到 for 循环中。
  • 我刚刚做了,但没有任何变化。

标签: java multithreading


【解决方案1】:

您在此处持有锁,因此一次只能打印一个进程

   synchronized (sync) {
        for (int i = 0; i < 10; i++) {
            sync.print(s);
        }
    }

您可以暂时释放锁,而不是这样做

   synchronized (sync) {
        for (int i = 0; i < 10; i++) {
            sync.print(s);
            // I have done my bit, wake other threads.
            sync.notifyAll();
            try {
                // give up the lock and let another thread run.
                sync.wait(10);
            } catch(InterruptedException ie) {
                throw new AssertionError(ie);
            }
        }
    }

您可能想到的就是我所说的乒乓球测试。您不会在实际程序中这样做,但这种模式是一个有用的微基准。

public class PingPongMain {
    public static void main(String[] args) throws InterruptedException {
        boolean[] next = {false};
        AtomicInteger count = new AtomicInteger();
        Thread t1 = new Thread(() -> {
            try {
                synchronized (next) {
                    for(;;) {
                        // handle spurious wake ups.
                        while (next[0])
                            next.wait();

                        System.out.println("ping");

                        // state change before notify
                        next[0] = true;
                        next.notifyAll();
                    }
                }
            } catch (InterruptedException e) {
                // expected
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                synchronized (next) {
                    for(;;) {
                        // handle spurious wake ups.
                        while (!next[0])
                            next.wait();

                        System.out.println("pong");

                        // state change before notify
                        next[0] = false;
                        next.notifyAll();

                        count.incrementAndGet();
                    }
                }
            } catch (InterruptedException e) {
                // expected
            }
        });

        t1.start();
        t2.start();

        Thread.sleep(5000);
        t1.interrupt();
        t2.interrupt();
        System.out.println("Ping ponged " + count + " times in 5 seconds");

    }
}

打印

ping
pong
ping
pong
.. deleted ...
Ping ponged 323596 times in 5 seconds

【讨论】:

  • 虽然,我讨厌评论一个如此小的神灵的答案,但这并不能保证像 OP 暗示他们在问题中想要的那样排序 - 整个程序结构需要更多的工作才能做到就是这样。
  • @AndyBrown 你是对的,尽管如果 OP 想要保证订单,他​​们应该使用一个带循环的线程。您应该只将线程用于大部分独立任务
  • @AndyBrown 我添加了一个更长的示例。
  • 非常好的ʷʰᶦˢᵖᵉʳˢʷʰᶦˢᵖᵉʳˢ⁻ᴵᵏᶰᵒʷᵗʰᵃᵗʸᵒᵘᵏᶰᵒʷᵗʰᵃᵗʸᵒᵘᶜᵃᶰᵖʳᶦᶰᵗᵗʰᵉᵗʰᵉᵖᵒᶰᵍᵉᵈᵖᵒᶰᵍᵉᵈ⋅⋅⋅ᵗᶦᵐᵉˢᶦᶰ⁵ˢᵉᶜᵒᶰᵈˢˢᵉᶜᵒᶰᵈˢᵇᵉᶠᵒʳᵉᵗʰᵉᵗʰʳᵉᵃᵈˢʰᵃᵛᵉˢᵗᵒᵖᵖᵉᵈˢᵗᵒᵖᵖᵉᵈ“ᵖᶦᶰᵍ”ᵃᶰᵈ“ᵖᵒᶰᵍ” span >
  • @AndyBrown 是的,尽管打印时计数是正确的。
【解决方案2】:

我猜你想模拟一个有两个独立线程访问同一资源的作业:例如System.out,但睡眠部分可能会同时运行。

在您的模拟中,您不应该将暂停放在同步块内:

public class SimpleSyncThread {

    public void print(String s) {
        synchronized(this){
           System.out.println(s);
        }
        MyThread.pause(200);
    }

在运行函数中,你不再需要同步了:

public void run() {
        for (int i = 0; i < 10; i++) {
            sync.print(s);
        }
}

现在,您将收到“Hello World Hello World”,或者可能是“Hello World World Hello”。

【讨论】:

    【解决方案3】:

    你举了一个难度不小的例子:java中通过waitnotify的基本同步旨在同步consumer-producer范式:有一些生产者线程和一些消费者线程。每个生产者无需等待即可完成其工作,然后唤醒(通知)正在等待通知的消费者线程。即使可运行类同时是消费者和生产者,该方案也可以使用。但是通信总是单向的

       producer -> consumer
    

    相反,您尝试做的是双向替代线程通信:

       producer -> consumer -> producer -> consumer ...
    

    我认为你需要一种更复杂的方式来传达你的线程:一个令牌管理器,一个包含从 0 到 N 的整数令牌的类,并旋转它:

    public class TokenManager
    {
        private final int maxTokens;
    
        private int token;
    
        public TokenManager(int maxTokens, int initialToken)
        {
            super();
            this.maxTokens=maxTokens;
            this.token=initialToken % this.maxTokens;
        }
    
        public int getToken()
        {
            return this.token;
        }
    
        public void nextToken()
        {
            this.token=++this.token % this.maxTokens;
        }
    }
    

    然后是一个可运行的类,它接收一个 TokenManager 并使用它进行同步:

    public class MyRunnable implements Runnable
    {
        private final String text;
    
        private final TokenManager token;
    
        // Identifier token value for this object.
        private final int id;
    
        public MyRunnable(int id, String text, TokenManager token)
        {
            super();
            this.id=id;
            this.text=text;
            this.token=token;
        }
    
        @Override
        public void run()
        {
            try
            {
                for (int i=0; i < 10; i++)
                {
                    synchronized (this.token)
                    {
                        // Wait until the TokenManager token is equal to the id of this object:
                        while (this.token.getToken() != this.id)
                        {
                            this.token.wait();
                        }
    
                        // Now it's our turn to print:
                        System.out.printf("%d: %s\n", i, this.text);
                        this.token.nextToken();
    
                        // Ask the TokenManager to progress to the next token:
                        this.token.notifyAll();
                    }
                }
            }
            catch (InterruptedException e)
            {
                throw new Error(e);
            }
        }
    }
    
    public static void main(String[] args)
    {
        // Instantiate the TokenManager for a specified number of threads:
        TokenManager token=new TokenManager(2, 0);
    
        // Instantiate and start the thread with id=0:
        new Thread(new MyRunnable(0, "Hello", token)).start();
        // Instantiate and start the thread with id=1:
        new Thread(new MyRunnable(1, "World", token)).start();
    }
    

    通过这种方式,主要方法通过将 ID 分配给实例化线程(按升序)来决定激活顺序。而且,如果您想要一种新类型的线程,您只需将 3 传递给 TokenManager(而不是 2)并使用正确的 ID 启动一个新线程:

        new Thread(new MyRunnable(2, "I'm here", token)).start();
    

    备注(感谢Andy Brown):

    • 令牌 ID 必须按顺序给出,不得有间隔。
    • 可能有多个线程具有相同的令牌 ID。在这种情况下,他们将在轮到他们时被随机处决。

    【讨论】:

    • 我喜欢这个——它是@PeterLawrey's answer 的多方扩展——尽管考虑到Semaphore 的存在,称它为Semaphore 可能会让人感到困惑。它应该带有健康警告,因为它可能需要进行工作以使其具有生产弹性(例如,每个令牌值一个所有者,只有连续的令牌值,否则它将永远等待......)。
    • 我已经用你的 cmets 更新了我的答案。谢谢。但是,每个令牌值可能有多个所有者。
    • 编辑:可能需要工作 → 可能需要工作,具体取决于预期的合同;)
    猜你喜欢
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多