三个线程交替顺序打印ABC

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

这是一个非常有意思的问题。本质上我们要让并发运行的三个线程能够感知其他线程的行为,进而控制自己的行为,达到整体有序。
这也是一个线程通信问题,怎么解决呢?
最简单的方案就是共享内存中放几把锁,运行中的线程只需要通过 竞争锁 这个行为就能感知其他线程。

有很多种方法实现

  1. 使用synchronized, wait和notifyAll
  2. 使用Lock->ReentrantLock 和 state标志
  3. 使用Lock->ReentrantLock 和Condition(await 、signal、signalAll)
  4. 使用Semaphore
  5. 使用AtomicInteger

详细可以参考 https://blog.csdn.net/hefenglian/article/details/82596072

我们看到很多实现中都是多个线程去执行同一个执行体,公共的执行体里面来进行锁的判断、序号的判断等等,
其实这些并不是必须的,接下来我们展示一个两个线程顺序打印AB的实例, 三个也是类似处理,就不写了。

package thread;

/**
 * @version jdk12
 */
public class Main {
    public static void main(String[] args) {
        Object o = new Object();
        int maxCount = 100;
        Object oa = new Object();
        Object ob = new Object();

        new Thread(() -> {
            synchronized (o) {
                o.notifyAll();
            }

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

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


        synchronized (o) {
            try {
                o.wait();
            } catch (InterruptedException e) {
            }
        }
        synchronized (oa) {
            oa.notify();  // 通知在oa上等待的线程, 在此之前,thread1和thread2处于死锁状态(类比两个哲学家就餐)
        }

        System.out.println("main done");
    }
}

输出

main done
thread1 A
thread2 B
thread1 A
thread2 B
thread1 A
thread2 B
...

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

1、wait()方法为什么要放在同步块中?
避免 lost wake up问题,就是如果wait 和 notify写在同一个对象中的话,会出现发送notify的之后,另外一个该对象的才刚刚调用wait方法,这就导致调用wait的对象一直无法被显式唤醒。

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

2、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”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章