三个线程交替顺序打印ABC

首先看下问题:
建立三个线程A、B、C,A线程打印10次字母A,B线程打印10次字母B,C线程打印10次字母C,但是要求三个线程同时运行,并且实现交替打印,即按照ABCABCABC的顺序打印。

这是一个有意思的问题。多线程本身是并发执行的,我们需要协调线程执行顺序,才能达到整体有序。这也是一个线程通信问题,怎么解决呢?
思路就是共享内存中放几把锁,多个线程通过竞争锁释放锁来协调执行顺序

Java线程在运行的生命周期中可能处于如下所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态
image
线程状态变迁如下:
image

这里写一个2个线程顺序打印AB的实例, 三个也是类似处理。有很多种方法实现(详细参考),这里使用Synchronized同步、wait、notify方法。

点击查看V1代码
package thread;

public class feb {
    public static void main(String[] args) {
        int maxCount = 100;  //多试几次,无法保证严格有序
        Object oa = new Object();
        Object ob = new Object();

        new Thread(() -> {
            for (int i = 0; i < maxCount; i++) {
                synchronized (oa) {
                    try {
                        oa.wait();
                        System.out.println(Thread.currentThread().getName() + " A " + i);
                    } catch (InterruptedException e) {
                    }
                }
                synchronized (ob) {
                    ob.notify();
                }
            }
        }, "thread-1").start();

        new Thread(() -> {
            for (int i = 0; i < maxCount; i++) {
                synchronized (ob) {
                    try {
                        ob.wait();
                        System.out.println(Thread.currentThread().getName() + " B " + i);
                    } catch (InterruptedException e) {
                    }
                }
                synchronized (oa) {
                    oa.notify();
                }
            }
        }, "thread-2").start();


        synchronized (oa) {
            oa.notify();  // 通知在oa上等待的线程, 在此之前,thread-1在oa上等待,thread-2在ob上等待,都处于 WAITING 状态
        }
        System.out.print("[main done]");
    }
}


thread-1首先在oa上等待,打印完之后唤醒ob上等待的thread-2,然后继续在oa上等待。 thread-2操作相反。
理想情况下这样没问题,但是实际执行中不稳定,还没打印完成进程就卡住了,通过jstack 可以看到thread-1和thread-2都处于 WAITING状态
因为可能会出现thread-1先notify唤醒,然后thread-2才开始wait等待。
V1代码不能保证同一时刻应该只有一个线程运行。


可以通过双重锁来保证执行顺序, V2代码如下

package thread;

public class feb2 {
    public static void main(String[] args) {
        int maxCount = 100;
        Object lockA = new Object();
        Object lockB = new Object();

        //也可以新建一个类,实现runnable对象的处理逻辑, 多个线程复用
        new Thread(() -> {
            for (int i = 0; i < maxCount; i++) {
                synchronized (lockB) {
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + " A " + i);
                        // 通知在lockA上等待的线程, thread-2 被唤醒,抢锁前状态为BLOCKED 抢锁后为 RUNNABLE
                        lockA.notify();
                    }
                    try {
                        // 符合条件就在lockA上等待,thread-1在lockB上等待,处于 WAITING 状态
                        if (i + 1 < maxCount) lockB.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "thread-1").start();

        new Thread(() -> {
            for (int i = 0; i < maxCount; i++) {
                synchronized (lockA) {
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + " B " + i);
                        lockB.notify();
                    }
                    try {
                        if (i + 1 < maxCount) lockA.wait(); //继续等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "thread-2").start();

        System.out.print("[main done]");
        //所有线程结束后,进程终止
    }
}

输出

thread-1 A 0
[main done]thread-2 B 0
thread-1 A 1
thread-2 B 1
thread-1 A 2
thread-2 B 2
thread-1 A 3
thread-2 B 3
……

基于这个问题还可以深入讨论

1、wait()方法为什么要放在同步块中?

https://www.jianshu.com/p/b8073a6ce1c0

在执行两个方法时,要先获得锁
避免 lost wake up问题,就是如果wait 和 notify写在同一个对象中的话,会出现发送notify的之后,另外一个该对象的才刚刚调用wait方法,这就导致调用wait的对象一直无法被显式唤醒。

2、wait和notify操作有什么区别呢?

https://www.jianshu.com/p/f79fa5aafb44

wait() 与 notify/notifyAll() 是Object类的方法,在执行两个方法时,要先获得锁。
当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态(等被唤醒后会恢复上下文,继续执行)。

当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
(从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。所以在实际编程中,我们应该尽量在线程调用notify/notifyAll()后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码)

wait 方法源码(当前线程把自己放在了对应object的wait set,然后释放对应object的锁)

 /**
     * Causes the current thread to wait until it is awakened, typically
     * by being <em>notified</em> or <em>interrupted</em>, or until a
     * certain amount of real time has elapsed.
     * <p>
     * The current thread must own this object's monitor lock. See the
     * {@link #notify notify} method for a description of the ways in which
     * a thread can become the owner of a monitor lock.
     * <p>
     * This method causes the current thread (referred to here as <var>T</var>) to
     * place itself in the wait set for this object and then to relinquish any
     * and all synchronization claims on this object. Note that only the locks
     * on this object are relinquished; any other objects on which the current
     * thread may be synchronized remain locked while the thread waits.
     * <p>
     * Thread <var>T</var> then becomes disabled for thread scheduling purposes
     * and lies dormant until one of the following occurs:
     * <ul>
     * <li>Some other thread invokes the {@code notify} method for this
     * object and thread <var>T</var> happens to be arbitrarily chosen as
     * the thread to be awakened.
     * <li>Some other thread invokes the {@code notifyAll} method for this
     * object.
     * <li>Some other thread {@linkplain Thread#interrupt() interrupts}
     * thread <var>T</var>.
     * <li>The specified amount of real time has elapsed, more or less.
     * The amount of real time, in nanoseconds, is given by the expression
     * {@code 1000000 * timeoutMillis + nanos}. If {@code timeoutMillis} and {@code nanos}
     * are both zero, then real time is not taken into consideration and the
     * thread waits until awakened by one of the other causes.
     * <li>Thread <var>T</var> is awakened spuriously. (See below.)
     * </ul>
     * <p>
     * The thread <var>T</var> is then removed from the wait set for this
     * object and re-enabled for thread scheduling. It competes in the
     * usual manner with other threads for the right to synchronize on the
     * object; once it has regained control of the object, all its
     * synchronization claims on the object are restored to the status quo
     * ante - that is, to the situation as of the time that the {@code wait}
     * method was invoked. Thread <var>T</var> then returns from the
     * invocation of the {@code wait} method. Thus, on return from the
     * {@code wait} method, the synchronization state of the object and of
     * thread {@code T} is exactly as it was when the {@code wait} method
     * was invoked.
     * <p>
     * A thread can wake up without being notified, interrupted, or timing out, a
     * so-called <em>spurious wakeup</em>.  While this will rarely occur in practice,
     * applications must guard against it by testing for the condition that should
     * have caused the thread to be awakened, and continuing to wait if the condition
     * is not satisfied. See the example below.
     * <p>
     * For more information on this topic, see section 14.2,
     * "Condition Queues," in Brian Goetz and others' <em>Java Concurrency
     * in Practice</em> (Addison-Wesley, 2006) or Item 69 in Joshua
     * Bloch's <em>Effective Java, Second Edition</em> (Addison-Wesley,
     * 2008).
     * <p>
     * If the current thread is {@linkplain java.lang.Thread#interrupt() interrupted}
     * by any thread before or while it is waiting, then an {@code InterruptedException}
     * is thrown.  The <em>interrupted status</em> of the current thread is cleared when
     * this exception is thrown. This exception is not thrown until the lock status of
     * this object has been restored as described above.
     *
     * @apiNote
     * The recommended approach to waiting is to check the condition being awaited in
     * a {@code while} loop around the call to {@code wait}, as shown in the example
     * below. Among other things, this approach avoids problems that can be caused
     * by spurious wakeups.
     *
     * <pre>{@code
     *     synchronized (obj) {
     *         while (<condition does not hold> and <timeout not exceeded>) {
     *             long timeoutMillis = ... ; // recompute timeout values
     *             int nanos = ... ;
     *             obj.wait(timeoutMillis, nanos);
     *         }
     *         ... // Perform action appropriate to condition or timeout
     *     }
     * }</pre>
     *
     * @param  timeoutMillis the maximum time to wait, in milliseconds
     * @param  nanos   additional time, in nanoseconds, in the range range 0-999999 inclusive
     * @throws IllegalArgumentException if {@code timeoutMillis} is negative,
     *         or if the value of {@code nanos} is out of range
     * @throws IllegalMonitorStateException if the current thread is not
     *         the owner of the object's monitor
     * @throws InterruptedException if any thread interrupted the current thread before or
     *         while the current thread was waiting. The <em>interrupted status</em> of the
     *         current thread is cleared when this exception is thrown.
     * @see    #notify()
     * @see    #notifyAll()
     * @see    #wait()
     * @see    #wait(long)
     */
    public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("timeoutMillis value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
            timeoutMillis++;
        }

        wait(timeoutMillis);
    }

3、notify()到底唤醒的是等到在object的wait set中的哪个线程?
结论:随机
被唤醒的线程恢复现场,继续工作

 /**
     * Wakes up a single thread that is waiting on this object's
     * monitor. If any threads are waiting on this object, one of them
     * is chosen to be awakened. The choice is arbitrary and occurs at
     * the discretion of the implementation. A thread waits on an object's
     * monitor by calling one of the {@code wait} methods.
     * <p>
     * The awakened thread will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened thread will
     * compete in the usual manner with any other threads that might be
     * actively competing to synchronize on this object; for example, the
     * awakened thread enjoys no reliable privilege or disadvantage in being
     * the next thread to lock this object.
     * <p>
     * This method should only be called by a thread that is the owner
     * of this object's monitor. A thread becomes the owner of the
     * object's monitor in one of three ways:
     * <ul>
     * <li>By executing a synchronized instance method of that object.
     * <li>By executing the body of a {@code synchronized} statement
     *     that synchronizes on the object.
     * <li>For objects of type {@code Class,} by executing a
     *     synchronized static method of that class.
     * </ul>
     * <p>
     * Only one thread at a time can own an object's monitor.
     *
     * @throws  IllegalMonitorStateException  if the current thread is not
     *               the owner of this object's monitor.
     * @see        java.lang.Object#notifyAll()
     * @see        java.lang.Object#wait()
     */
    @HotSpotIntrinsicCandidate
    public final native void notify();

4、什么是虚假唤醒 (spurious wakeup)?

在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。换句话说,等待应总是发生在循环中,如下面的示例:

synchronized (obj) {
while ()
obj.wait(timeout);
... // Perform action appropriate to condition
}
(有关这一主题的更多信息,请参阅 Doug Lea 撰写的《Concurrent Programming in Java (Second Edition)》(Addison-Wesley, 2000) 中的第 3.2.3 节或 Joshua Bloch 撰写的《Effective Java Programming Language Guide》(Addison-Wesley, 2001) 中的第 50 项。

在POSIX Threads中(spurious wakeup):

David R. Butenhof 认为多核系统中 条件竞争(race condition )导致了虚假唤醒的发生,并且认为完全消除虚假唤醒本质上会降低了条件变量的操作性能。

“…, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations. The race conditions that cause spurious wakeups should be considered rare”

posted @ 2019-12-08 20:20  畑鹿驚  阅读(4617)  评论(4编辑  收藏  举报