【问题标题】:Java multithreading wait and notify in same blockJava多线程在同一个块中等待和通知
【发布时间】:2019-10-11 00:40:14
【问题描述】:

我正在解决一个小问题,我需要使用两个线程以交替方式顺序打印数字。就像线程 1 打印 1,线程 2 打印 2,线程 1 打印 3 等等......

所以我在下面创建了一段代码,但在某些时候,两个线程都进入了等待状态,控制台上什么也没有打印出来。

import java.util.concurrent.atomic.AtomicInteger;

public class MultiPrintSequence {

    public static void main(String[] args) {
        AtomicInteger integer=new AtomicInteger(0);
        Sequence sequence1=new Sequence(integer);
        Sequence sequence2=new Sequence(integer);
        sequence1.start();
        sequence2.start();
    }
}

class Sequence extends Thread{

    private AtomicInteger integer;
    boolean flag=false;

    public Sequence(AtomicInteger integer) {
        this.integer=integer;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (integer) {
                while (flag) {
                    flag=false;
                    try {
                        System.out.println(Thread.currentThread().getName()+" waiting");
                        integer.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+" "+integer.incrementAndGet());
                flag = true;
                System.out.println(Thread.currentThread().getName()+" notifying");
                integer.notify();
            }
        }
    }
}

在观察控制台输出时,我注意到在某个时候,当一个线程发出通知时,另一个线程最终甚至在通知线程进入等待状态之前就启动了,因此在某一时刻,两个线程都进入了等待状态。下面是控制台输出的一小部分。

Thread-1 510
Thread-1 notifying
Thread-1 waiting
Thread-0 511
Thread-0 notifying
Thread-0 waiting
Thread-1 512
Thread-1 notifying
Thread-1 waiting
**Thread-0 513
Thread-0 notifying
Thread-1 514
Thread-1 notifying
Thread-1 waiting
Thread-0 waiting**

【问题讨论】:

  • 共享可变状态,这就是问题所在。你有很好的意识来正确地共享整数,但不是为了标志。
  • 实际上我刚刚遇到了问题,是的,我们可以使标志在两个线程之间共享。但这并不是真正的问题。我的 while 块结构很差。 while(true) 块应该在同步块内,并用 if 替换 while(flag) ,我们就完成了。这是一次严重的失误。但是,代码也适用于您的解决方案,谢谢 :)
  • IMO,您需要更加仔细地思考flag 的含义。您的两个线程都执行相同的代码,并且它们都被赋予相同的参数。它们是相同的。但他们应该是不同的。其中一个应该只打印奇数,另一个应该只打印偶数。您需要一种方法来告诉线程,“您的工作是只打印偶数”(或“奇数”);并且您需要一种方法让每个线程等到打印偶数/奇数的时间。
  • P.S.,为了额外的功劳,找出为什么这个问题与线程旨在解决的问题几乎完全相反。 (提示,与线程有关的一切都与“并发”这个词有关。)

标签: java multithreading java-threads


【解决方案1】:

考虑一下这一系列不幸的事件。 Thread1 递增该值,将标志设置为 true 并通知等待集中的任何线程以获得锁。现在 Thread0 已经在等待集中。然后 Thread0 唤醒,它的标志 = false。然后 Thread0 退出 while 循环并打印增加的值并通知任何等待的线程。然后它继续进行 while 循环中的下一次迭代,并在锁定对象上调用 wait。但是 Thread1 并没有处于等待状态,而是在完成同步块后被调度程序切换出 CPU 给 Thread0 机会。 Thread1 处于可运行状态,并且由于没有可运行的线程剩余,因此调度程序再次给了机会。然后 Tread1 由于 flag = true 进入 while 循环,并在同一个锁对象上调用 wait。现在两个线程都处于等待状态,没有人唤醒它们。所以这是系统中活锁的一个很好的例子。

发生这种情况是因为标志是一个实例字段,因此不在线程之间共享。所以每个线程都有自己的标志副本。如果将其标记为静态变量,则两个线程共享该值,因此问题得到解决。这是标志声明的外观。

static boolean flag = false;

这如何解决问题?好吧,考虑相同的事件序列。现在 Thread1 在调用锁对象上的 notify 之前将标志值设置为 true。 Thread0 已经处于等待状态。调度程序将 Thread1 关闭 CPU,并为 Thread0 提供机会。它开始运行,并且由于 flag = true,它进入 while 循环,将 flag 设置为 false 并在锁定对象上调用 wait。然后 Thread0 进入等待状态,schedular 给 Thread1 一个机会。 Thread1 恢复它的执行并且 flag = false,因此它退出 while 循环,打印增加的值并通知等待的线程。所以现在没有活锁。

但是我看不出同时使用synchronized 和非阻塞原子变量有什么意义。你不应该同时使用它们。下面给出了一个更好、性能更高的实现。

public class Sequence extends Thread {
    private static final Object lock = new Object();
    private static int integer = 0;
    static boolean flag = false;

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                while (flag) {
                    flag = false;
                    try {
                        System.out.println(Thread.currentThread().getName() + " waiting");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + " " + ++integer);
                flag = true;
                System.out.println(Thread.currentThread().getName() + " notifying");
                lock.notify();
            }
        }
    }

    public static void main(String[] args) {
        Sequence sequence1=new Sequence();
        Sequence sequence2=new Sequence();
        sequence1.start();
        sequence2.start();
    }
}

【讨论】:

    【解决方案2】:

    您的flag 变量不在线程之间共享,但是围绕该标志的逻辑无论如何都很奇怪。请注意,当您使用synchronized 时,您不需要使用AtomicInteger。

    当正确使用synchronized时,一个普通的int变量已经足以实现整个逻辑:

    public class MultiPrintSequence {
        public static void main(String[] args) {
            final Sequence sequence = new Sequence();
            new Thread(sequence).start();
            new Thread(sequence).start();
        }
    }
    class Sequence implements Runnable {
        private final Object lock = new Object();
        private int sharedNumber;
    
        @Override
        public void run() {
            synchronized(lock) {
                for(;;) {
                    int myNum = ++sharedNumber;
                    lock.notify();
                    System.out.println(Thread.currentThread()+": "+myNum);
                    while(sharedNumber == myNum) try {
                        lock.wait();
                    } catch (InterruptedException ex) {
                        throw new AssertionError(ex);
                    }
                }
            }
        }
    }
    

    当然,创建多个线程来执行顺序操作违背了并发编程的实际目的。

    【讨论】:

      【解决方案3】:

      在代码中,即使整数是原子的并且在线程之间共享,标志本身也不是。

      class Sequence extends Thread{
      
          private AtomicInteger integer; //shared
          boolean flag=false; //local
      
          public Sequence(AtomicInteger integer) {
            this.integer=integer;
          }
      

      这会导致一个线程的变化没有反映在另一个线程中。

      建议的解决方案:

      您也可以使用 Atomic 解决 flag 和共享,例如:

      import java.util.concurrent.atomic.AtomicBoolean;
      import java.util.concurrent.atomic.AtomicInteger;
      
      public class StackOverflow {
      
          public static void main(String[] args) {
              AtomicInteger integer=new AtomicInteger(0);
              AtomicBoolean flag=new AtomicBoolean(true);
              Sequence sequence1=new Sequence(integer, flag);
              Sequence sequence2=new Sequence(integer, flag);
              sequence1.start();
              sequence2.start();
          }
      }
      

      还有顺序:

      import java.util.concurrent.atomic.AtomicBoolean;
      import java.util.concurrent.atomic.AtomicInteger;
      
      class Sequence extends Thread{
      
          private final AtomicInteger integer;
          private AtomicBoolean flag;
      
          public Sequence(AtomicInteger integer, AtomicBoolean flag) {
              this.integer=integer;
              this.flag=flag;
          }
      
          @Override
          public void run() {
              while(true) {
                  synchronized (integer) {
                      while (flag.get()) {
                          flag.set(false);
                          try {
                              System.out.println(Thread.currentThread().getName()+" waiting");
                              integer.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      System.out.println(Thread.currentThread().getName()+" "+integer.incrementAndGet());
                      flag.set(true);
                      System.out.println(Thread.currentThread().getName()+" notifying");
                      integer.notify();
                  }
              }
          }
      }
      

      这是输出的一部分:

      Thread-1 8566
      Thread-1 notifying
      Thread-1 waiting
      Thread-0 8567
      Thread-0 notifying
      Thread-0 waiting
      Thread-1 8568
      Thread-1 notifying
      Thread-1 waiting
      Thread-0 8569
      Thread-0 notifying
      Thread-0 waiting
      

      【讨论】:

      • 实际上我刚刚遇到了问题,是的,我们可以像在两个线程之间那样使标志成为可共享的。但这并不是真正的问题。我的 while 块结构很差。 while(true) 块应该在同步块内,并用 if 替换 while(flag) ,我们就完成了。这是一次严重的失误。但是,代码也适用于您的解决方案,谢谢 :)
      • 你应该解释它是如何解决这个问题的。但我在这里没有看到这种解释。
      • 使用两种不同的同步机制有什么意义?为什么不能使用静态变量?
      • 但我仍然没有看到第一个问题的合理答案,即“使用两个不同的同步机器有什么意义?”在同步块中,原子性得到保证。那么在那里使用另一个原子变量有什么意义呢?使用它的目的是什么?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-06
      相关资源
      最近更新 更多