三個線程交替順序打印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”

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