Java 多线程在同一块中等待和通知

Java multithreading wait and notify in same block

我正在解决一个小问题,我需要用两个线程以交替方式顺序打印数字。就像线程 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**

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

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

static boolean flag = false;

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

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

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();
    }
}

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

class Sequence extends Thread{

    private AtomicInteger integer; //shared
    boolean flag=false; //local

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

这会导致一个线程中的更改不会反映在另一个线程中。

建议的解决方案:

你也可以解决使用 Atomic 作为标志并共享,例如:

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class Whosebug {

    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

您的 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);
                }
            }
        }
    }
}

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