1. Java線程排程
線程排程是指系統為線程配置設定處理器使用權的過程,主要排程方式由兩種,分别是協同式線程排程和搶占式線程排程。Java使用的線程排程方式是搶占式排程。
1.1 協同式排程
如果使用協同式排程的多線程系統,線程的執行時間由線程本身來控制,線程把自己工作執行完了值後,要主動通知系統切換到另一條線程上。協同式多線程的最大好處的實作簡單,由于線程要把自己的事情幹完之後才會進行線程切換,切換操作對線程自己可知的,是以沒有什麼線程同步的問題。壞處是線程執行時間不可控制,甚至如果一個線程編寫有問題,一直不告知系統進行線程切換,那麼程式就會一直阻塞在那裡。
1.2 搶占式排程
如果使用搶占式排程的多線程系統,那麼每個線程将由系統來配置設定執行時間,線程的切換不由線程本身來決定。在這種實作線程排程的方式下,線程的執行時間是可控的,也不會有一個線程導緻整個程序阻塞的問題。
1.3 Java排程輔助機制
1.3.1 線程優先級
Java使用的線程排程方式是搶占式排程,但是可以通過線程優先級來給作業系統"建議"給某些線程多配置設定一點執行時間。
Java語言一共設定了10個級别的線程優先級(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在兩個線程同時處于Ready狀态時,優先級越高的線程越容易被系統選擇執行。但是作業系統的線程優先級與Java語言的線程優先級并不能一一對應,而且優先級可能會被系統自行改變。
1.3.2 睡眠
線程睡眠的方法有Thread.sleep(long millis)、重載方法Thread.sleep(long millis, int nanos)方法以及可讀性更好的TimeUnit枚舉類封裝的sleep(long timeout)方法。
下面是Thread類的sleep(long millis)方法API。
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
從注釋上可以看到該方法的作用能讓目前正在執行的線程休眠(暫時停止執行)指定的毫秒數,這句話的意思是目前線程在一定時間内讓出自己CPU的所有權;具體休眠多長時間取決于系統計數器和排程程式的精度和準确性。這句話的意思是,傳入參數為100時,目前線程實際可能會休眠100ms,也有可能實際休眠不是100ms,取決于作業系統;該線程不會失去任何管程的所有權,這句話的意思是線程不會釋放鎖。其中線程會抛出非法參數異常以及中斷異常。
需要注意的是線程休眠時會放出自己的CPU的所有權,但是不會放棄鎖,此時線程進入到限期等待狀态。休眠結束後,并不一定會立即獲得CPU的所有權,此時線程會進入Ready狀态,而非Runing狀态。簡單而言就是就緒狀态,等待CPU重新排程、配置設定執行時間片。具體内容見線程生命周期。
其中Thread.sleep(0)的作用是線程放棄CPU的所有權,等待作業系統重新排程,與Thread.yield()作用相同,相關試驗見讓步章節。
下面是Thread類的sleep(long millis, int nanos)方法的實作。
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds plus the specified
* number of nanoseconds, subject to the precision and accuracy of system
* timers and schedulers. The thread does not lose ownership of any
* monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @param nanos
* {@code 0-999999} additional nanoseconds to sleep
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative, or the value of
* {@code nanos} is not in the range {@code 0-999999}
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
可以看到底層是調用了sleep(long millis)方法。在這裡看jdk的實作還是比較皮的,個人觀點是這樣的:底層作業系統做不到休眠納秒級别或者不需要精确到納秒級别。并且可以看出,休眠的最小時間機關是毫秒。
下面是TimeUnit類的 sleep(long timeout)方法的實作。
/**
* Performs a {@link Thread#sleep(long, int) Thread.sleep} using
* this time unit.
* This is a convenience method that converts time arguments into the
* form required by the {@code Thread.sleep} method.
*
* @param timeout the minimum time to sleep. If less than
* or equal to zero, do not sleep at all.
* @throws InterruptedException if interrupted while sleeping
*/
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}
TimeUnit類是一個枚舉類,具體即為時間機關的枚舉。這個方法底層是調用了Thread類的sleep(long millis, int nanos)方法。具體作用是為了可讀性更好。如Thread.sleep(60000)是睡眠1分鐘的意思,使用TimeUnit.MINUTES.sleep(1)更加簡潔易懂。
1.3.3 等待
線程等待的方法主要包括Object類的wait(long timeout)方法及兩個重載方法wait()、wait(long timeout, int nanos);可讀性更好的TimeUnit類的timedWait(Object obj, long timeout)方法;底層操作類Unsafe類的park(boolean var1, long var2)方法;LockSupport類中park()方法、park(Object blocker)方法、parkNanos(long nanos)方法、parkNanos(Object blocker, long nanos)方法、parkUntil(long deadline)方法、parkUntil(Object blocker, long deadline)方法;Condition接口的await()方法等等。
不過需要注意的是:TimeUnit類的timedWait(Object obj, long timeout)方法底層是通過調用Object類的wait(long timeout, int nanos)方法實作的;Condition接口的await()等方法在實作類的中具體實作是通過調用LockSupport類中的park(Object blocker)等方法實作的;而LockSupport類中park()方法底層是通過Unsafe類的park(boolean var1, long var2)方法實作的。通常用的比較多的是Object類wait()方法以及LockSupport類中的park(Object blocker)方法。
下面根據Object類的wait(long timeout)的API了解下wait(long timeout)的作用與性質,根據注釋簡單介紹下wait(long timeout)主要做了什麼事。
/**
* Causes the current thread to wait until either another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or a
* specified amount of time has elapsed.
* <p>
* The current thread must own this object's monitor.
* <p>
* This method causes the current thread (call it <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. Thread <var>T</var>
* becomes disabled for thread scheduling purposes and lies dormant
* until one of four things happens:
* <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. If
* {@code timeout} is zero, however, then real time is not taken into
* consideration and the thread simply waits until notified.
* </ul>
* The thread <var>T</var> is then removed from the wait set for this
* object and re-enabled for thread scheduling. It then competes in the
* usual manner with other threads for the right to synchronize on the
* object; once it has gained 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 also wake up without being notified, interrupted, or
* timing out, a so-called <i>spurious wakeup</i>. 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. In other words,
* waits should always occur in loops, like this one:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait(timeout);
* ... // Perform action appropriate to condition
* }
* </pre>
* (For more information on this topic, see Section 3.2.3 in Doug Lea's
* "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
* 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
* Language Guide" (Addison-Wesley, 2001).
*
* <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. This exception is not
* thrown until the lock status of this object has been restored as
* described above.
*
* <p>
* Note that the {@code wait} method, as it places the current thread
* into the wait set for this object, unlocks only this object; any
* other objects on which the current thread may be synchronized remain
* locked while the thread waits.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @param timeout the maximum time to wait in milliseconds.
* @throws IllegalArgumentException if the value of timeout is
* negative.
* @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 for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final native void wait(long timeout) throws InterruptedException;
該方法會導緻目前線程T暫停等待直到該對象調用notify()或者notifyAll()方法,或者指定的逾時時間已過。這句話的意思,執行object.wait()方法的線程T,注意不是object對象,是目前正在執行這行代碼的線程T會挂起等待,直到被同一個對象(這裡的object對象是一個鎖,含義是同一個鎖)的notify()或notifyAll()方法喚醒,或者需要等待的時間耗盡,自動退出等待。
目前線程T必須獲得目前對象的管程。這裡的意思是目前線程T必須擷取這個對象(調用wait()方法的Object類執行個體對象)的鎖,隻有擷取到該對象的鎖才能調用wait()方法。具體表現為必須在同步方法或者同步塊裡才能調用wait()方法,不在同步塊或者同步方法裡調用wait()方法,會抛出IllegalMonitorStateException。
調用該方法會導緻目前線程T将自身置于該對象的wait set中,然後放棄該對象上的所有同步聲明。了解這句話先了解一個知識點:每個對象都有一個與之關聯的唯一的鎖,也可以稱為管程(Monitor)。并且JVM會為每個鎖,即為每個對象關聯兩個集合(其實不止兩個,還有一個cxq隊列,這裡不提因為太深入,暫時還沒搞懂),分别為entry set和wait set。entry set存儲了等待擷取該對象關聯的鎖的所有線程;而wait set存儲了調用了wait()、wait(long timeout)、wait(long timeout, int nanos)、timedWait(Object obj, long timeout)方法的所有線程。是以這裡的意思是目前線程T持有該對象的鎖,調用該方法後,目前線程T将自身置于wait set中,然後釋放該對象的鎖。重點在于執行wait()方法前目前線程必定持有該對象的鎖,執行方法後,目前線程釋放了該對象的鎖。最重要的是目前線程隻會放棄調用wait()方法對象的鎖!如果線程T還持有其他對象的鎖,則線程T不會釋放,在等待期間會一直持有。
線程T在發生以下情況之一前都處于等待狀态(即發生以下四種情況時線程退出等待狀态):
其他線程調用該對象的notify()方法,并且線程T被排程器選擇作為喚醒的線程;
其他線程調用該對象的notifyAll()方法;
其他線程中斷了線程T;
線程進入等待狀态已經到達逾時時間。
PS:其中逾時為0,即調用方法wait()、wait(0)等方法時,則等待線程不會考慮逾時時間,隻會等待喚醒或中斷。
發生了上述四種情況後等待線程被喚醒,重新被排程器排程。喚醒後的線程可能加入到entry set中,也有可能在cxq隊列中,與entry set中的其他的線程一起競争鎖,如果線程T競争到鎖,此時對象上的鎖狀态都将恢複原狀——調用wait()方法時的情況。
線程也可以在沒有被通知,中斷或逾時的情況下喚醒,即所謂的虛假喚醒。 防止虛假喚醒的政策是在wait()傳回後還需要進一步判斷是否符合要求,即等待應該總是出現在循環中,如下所示為wait()方法的正确用法:
synchronized(obj){
while(條件不成立){
obj.wait(逾時);
}
...//執行适合條件的操作
}
此外目前線程在等待之前或者等待時被任何線程中斷,則抛出InterruptedException。但是線上程未擷取到鎖的時候,是不會抛出該中斷異常的,直到被中斷的線程擷取到鎖,InterruptedException才會被抛出。此外,抛出此異常時,目前線程的中斷狀态将被重置。具體試驗請見demo。
public class Main {
private static final Object lockObject = new Object();
static class TaskA implements Runnable{
@Override
public void run() {
synchronized (lockObject){
try {
System.out.println(System.currentTimeMillis() + ":線程"
+ Thread.currentThread().getName() + " sleep開始 線程A獲得鎖" );
Thread.sleep(5000);
System.out.println(System.currentTimeMillis() + ":線程"
+ Thread.currentThread().getName() + " sleep結束 線程A釋放鎖" );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class TaskB implements Runnable{
@Override
public void run() {
synchronized (lockObject){
try {
System.out.println(System.currentTimeMillis() + ":線程"
+ Thread.currentThread().getName() + " wait開始 線程B釋放鎖");
lockObject.wait();
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + ":線程B中斷狀态:"
+ Thread.currentThread().isInterrupted() + " 抛出異常後狀态重置");
//此時擷取到鎖才會抛出中斷異常
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable taska = new TaskA();
Runnable taskb = new TaskB();
Thread threada = new Thread(taska, "A");
Thread threadb = new Thread(taskb, "B");
threadb.start();
Thread.sleep(1000);//保證線程B已經釋放鎖
threada.start();
Thread.sleep(1000);//保證線程A已經獲得鎖以後才開始中斷B
threadb.interrupt();
System.out.println(System.currentTimeMillis() + ":線程B中斷狀态:"
+ threadb.isInterrupted() + " 抛出異常前狀态未重置");
}
}
下面給出預期結果,注意列印時的時間戳:
1562697214833:線程B wait開始 線程B釋放鎖
1562697215833:線程A sleep開始 線程A獲得鎖
1562697216834:線程B中斷狀态:true 抛出異常前狀态未重置
1562697220833:線程A sleep結束 線程A釋放鎖
1562697220833:線程B中斷狀态:false 抛出異常後狀态重置
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at Main$TaskB.run(Main.java:33)
at java.lang.Thread.run(Thread.java:748)
wait()方法、wait(long timeout, int nanos)方法以及timedWait(Object obj, long timeout)方法不再贅述,底層都是調用wait(long timeout)實作。這裡隻給出API實作。
wait()方法:
public final void wait() throws InterruptedException {
wait(0);
}
wait(long timeout, int nanos)方法:
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
timedWait(Object obj, long timeout)方法:
public void timedWait(Object obj, long timeout)
throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
obj.wait(ms, ns);
}
}
關于Unsafe類的park(boolean var1, long var2)方法與wait()方法實作功能類似,底層是利用了Posix的mutex,condition來實作的阻塞,而wait()底層利用了moniter實作的阻塞。在這裡不進行深入介紹(PS:這裡要看jdk源碼,因為都是native方法 ? )。
1.3.4 喚醒
線程喚醒的方法主要包括Object類的notify()方法及notifyAll()方法;底層操作類Unsafe類的unpark(Object var1)方法;LockSupport類中unpark(Thread thread)方法;Condition接口的signal()方法及signalAll()方法。
其中Condition接口的signal()方法及signalAll()方法在實作類的中具體實作是通過調用LockSupport類中的unpark(Thread thread)方法實作的;而LockSupport類中unpark(Thread thread)方法底層是通過Unsafe類的unpark(Object var1)方法實作的。通常用的比較多的是Object類notify()方法及notifyAll()方法以及LockSupport類中的unpark(Thread thread)方法。
下面根據Object類的notify()方法的API了解下notify()的作用與性質,根據注釋簡單介紹下notify()主要做了什麼事。
/**
* 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()
*/
public final native void notify();
該方法會喚醒在該對象鎖上等待的一個線程。如果有多個線程同時在等待同一個對象鎖,則會選擇其中一個線程喚醒;這句話的意思是notify()方法會喚醒由調用wait()方法而導緻進入wait set中的一個線程,并且無論等待的線程有多少,僅會喚醒一個線程。選擇線程喚醒的過程是任意的,當然這個這個“任意的”選擇過程實質上是由排程的具體實作決定的。
被喚醒的線程無法直接執行。直到目前持有鎖的線程釋放對象鎖。被喚醒的線程與其他就緒狀态的線程一起競争被目前持有鎖的線程釋放的鎖,并且被喚醒的線程在競争的過程中沒有特權也沒有劣勢。這句話的意思是被喚醒的線程不是立即就能擷取到CPU的所有權的,還是要等待目前線程執行完synchronized同步塊(方法)釋放鎖,然後與entry set中的線程一起競争這個鎖的,競争的過程排程器不會因為這個線程是被喚醒的而産生差别待遇。
該方法隻能由擷取到鎖的線程才能調用。這裡的意思與wait()方法相同,及notify()方法隻能在同步塊或者同步方法中調用,否則會抛出IllegalMonitorStateException。
notifyAll()方法與notify()方法需要注意的點大緻相同,也是喚醒由調用wait()等方法而進入等待狀态的線程,不過該方法會喚醒在wait set中的所有線程。
/**
* Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the
* {@code wait} methods.
* <p>
* The awakened threads will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened threads
* will compete in the usual manner with any other threads that might
* be actively competing to synchronize on this object; for example,
* the awakened threads enjoy 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. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notify()
* @see java.lang.Object#wait()
*/
public final native void notifyAll();
這裡signal()方法與unpark()方法不深入了解。具體請見Condition接口實作類中的signal()具體實作,以及native方法源碼。
1.3.5 讓步
線程讓步的方法是Thread類的yield()方法。
下面是Thread類yield()方法的API。從注釋上可以看到該方法的作用是目前線程向排程器發出一個提示,這個提示是自己讓出目前正在使用的處理器,這句話的意思是目前線程願意讓出自己CPU的所有權;排程器可以忽略此提示,這句話的意思排程器不會讓目前線程讓出CPU的所有權,目前線程仍然可以持有自己CPU的所有權。後面注釋是說該方法可以用于改善線程過度使用CPU的情況。而且使用該方法很少時候是合适的,但是可能對于測試、調試程式很有用。
需要注意的是線程讓步時會可能會讓出自己的CPU的所有權,但是不會放棄鎖,讓出CPU使用權後線程會進入Ready狀态,等待CPU重新排程、配置設定執行時間片。具體内容見線程生命周期。
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
在休眠時提到Thread.sleep(0)與Thread.yield()類似。下面是關于sleep(0)和yield()的兩個小小的測驗。涉及到線程的優先級以及sleep(0)和yield()的比較。
以下為yield()試驗:
public class Main {
public static void main(String[] args) {
Runnable yieldTask = new YieldTask();
Thread threada = new Thread(yieldTask, "Thread A");
Thread threadb = new Thread(yieldTask, "Thread B");
threada.setPriority(Thread.MIN_PRIORITY);
threadb.setPriority(Thread.MAX_PRIORITY);
threada.start();
threadb.start();
}
}
class YieldTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
Thread.yield();
}
}
}
下面是yield()多次試驗中出現的一次結果:
Thread A0
Thread B0
Thread A1
Thread B1
Thread A2
Thread B2
以下為sleep(0)試驗:
public class Main {
public static void main(String[] args) {
Runnable sleepTask = new SleepTask();
Thread threada = new Thread(sleepTask, "Thread A");
Thread threadb = new Thread(sleepTask, "Thread B");
threada.setPriority(Thread.MIN_PRIORITY);
threadb.setPriority(Thread.MAX_PRIORITY);
threada.start();
threadb.start();
}
}
class SleepTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + i);
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
下面是sleep(0)多次試驗中出現的一次結果:
Thread B0
Thread A0
Thread B1
Thread A1
Thread B2
Thread A2
上述兩個試驗說明兩件事:
Thread.sleep(0)不是不做任何事,調用了sleep(0)的線程會釋放自己的CPU所有權,然後等待CPU重新配置設定時間片。yield()做了同樣的事情。
無論是sleep()還是yield(),在釋放了CPU的所有權以後,無論其他線程比目前線程的優先級是高還是低,都有機會獲得CPU的所有權,線程優先級隻是排程考慮的因素之一。一個錯誤觀點是yield()隻會讓步給優先級相同或者比它高的線程,正确觀點是無論優先級高低,都有機會獲得CPU所有權,關鍵看排程器如何排程。
1.3.6 合并
線程合并的方法主要包括Thread類的join(long millis)方法及兩個重載方法join(long millis, int nanos)、join();可讀性更好的TimeUnit類的timedJoin(Thread thread, long timeout)方法。在這裡join()方法有多種解釋如線程加入、線程插隊、線程合并,但都是一個意思。
下面根據Thread類的join(long millis)方法的API了解下join(long millis)的作用與性質,首先根據注釋簡單介紹下join(long millis)主要做了什麼事。
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
注釋中提到此線程最多等待到逾時時間,如果逾時設定為0意味着永遠等待。這句話可能不太好了解,首先我們要厘清,是哪個線程在等待哪個線程。在調用join()方法時,至少有兩個線程:由于join()方法是Thread類的執行個體方法,即第一個線程是該執行個體線程,對應到下列demo中即為threadb;而另一個線程則是執行join()方法這行代碼的線程,對應到下列demo中即為main線程。在這裡使用了join()方法讓threadb與main線程産生了聯系:main線程會永遠等待threadb線程,直到threadb線程死亡。注意在這裡與threada線程沒有任何關系,threada的執行順序沒有受到任何限制。
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.print(Thread.currentThread().getName());
Thread threada = new Thread(task, "A ");
Thread threadb = new Thread(task, "B ");
threada.start();
threadb.start();
threadb.join();
System.out.print("Main ");
}
}
是以上述demo隻會有三種結果:
1. A B Main
2. B A Main
3. B Main A
join()方法的實作是使用以this.isAlive()方法為條件的内部this.wait()方法的調用循環。當一個線程終止退出時,會調用this.notifyAll()方法。此外建議不要在Thread執行個體上使用wait()、notify()、notifyAll()方法。
通過閱讀join(long millis)方法的具體實作可以知道内部其實是調用wait(long timeout)方法實作的。對應上述demo來說就是main線程擷取到threadb對象的鎖,執行wait(long millis)方法,導緻main線程進入等待直到目前執行完程式退出。
 在這裡可以看到join(long millis)方法是個同步方法,因為wait(long timeout)方法的調用必須在同步塊或者同步方法内。其中逾時不為0很好了解:wait(long timeout)方法逾時時間到了自動喚醒該線程。逾時為0不好了解,此時線程永遠等待,程式會一直阻塞。但是實際情況不會如此,原因在于,線程在終止退出時會自動調用notifyAll()方法,喚醒阻塞在目前線程對象鎖上的所有線程,在這裡JVM保證了調用join()方法不會永遠阻塞,具體請見線程的本地實作中exit()方法調用了ensure_join()方法,而在ensure_join()方法的實作中調用了 lock.notify_all(thread)。
join(long millis, int nanos)方法、join()方法以及timedJoin(Thread thread, long timeout)方法不再贅述,底層都是調用join(long millis)實作。這裡隻給出API實作。
join(long millis, int nanos)方法:
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
join()方法:
public final void join() throws InterruptedException {
join(0);
}
timedJoin(Thread thread, long timeout)方法:
public void timedJoin(Thread thread, long timeout)
throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
thread.join(ms, ns);
}
}
1.3.7 中斷
線程中斷是一個計算機術語,用于線上程在執行的過程中,由于發生了異常事件而導緻不能正常運作,需要終止線程,在此,通知目前運作中的線程終止的操作通常被稱為中斷操作。由概念我們了解到中斷本質上是希望終止線程。但是在Java語言中,線程中斷 != 線程終止。
在了解中斷之前,我們先來了解線程終止。提到線程終止,會首先想到一個在jdk6中被廢棄的方法:Thread類的stop()方法。在這裡就不貼出該方法注釋即API了,有興趣的可以自己檢視下。首先這個方法的作用就是強制線程停止執行。注意這裡的強制,當某個線程的執行個體調用stop()方法後,一定會停止(世上沒有絕對一定的事情,這裡不提及那些幾乎不會出現的情況),無論程式是否正常執行完。這裡就相當于君要臣死,臣不得不死,線程沒有決定權。但是,這個方法本質上是不安全的。在oracle官方文檔Java Thread Primitive Deprecation提及強制結束線程會導緻線程競争得到的鎖所保護的對象會處于不一緻狀态,即失去可見性,是以stop()方法被廢棄。
是以jdk6後就把線程終止的權利交由線程自身實作,即為中斷。線程對于是否終止自己具有決定權,線程自己怎麼實作終止被稱為中斷政策。而Java語言提供了三個API來幫助程式員自己實作中斷政策,即Thread類的interrupt()方法、interrupted()方法、isInterrupted()方法。簡單來說就是被中斷的線程調用interrupt()方法,然後線上程的run()方法中通過對interrupted()方法、isInterrupted()方法的檢測根據實際業務政策判斷是否終止自己。本質上來說目前線程通過被中斷線程執行個體調用interrupt()方法隻發送通知,不采取任何操作;被中斷的線程在特定的位置(安全點)檢查是否收到中斷通知,當檢測到通知後的操作(中斷政策)由被中斷線程(程式員)決定。被中斷線程可以選擇不理會中斷通知繼續執行,也可以選擇理會中斷通知做些其他操作(記錄日志,抛出異常,退出線程等)。
下面根據Thread類的interrupt()方法、interrupted()方法、isInterrupted()方法的API了解下中斷的作用與性質。
/**
* Interrupts this thread.
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
*
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p>
*
* <p> Interrupting a thread that is not alive need not have any effect.
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
根據interrupt()方法注釋了解到該方法的作用就是中斷指定線程,然後提到除非目前線程自己中斷自己,否則都需要權限,否則抛出SecurityException。
後續提到了由于執行阻塞方法而被阻塞的線程怎麼進行中斷。注釋在這裡提到了三種情況:
A. 如果線程被阻塞是由于Object類的wait()、wait(long)、wait(long, int) 方法或者Thread類的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法時,則被阻塞的線程的中斷标志将被清除,并将抛出InterruptedException。
B. 如果線程被阻塞是由于在java.nio.channels.InterruptibleChannel上的I/O操作時,則channel會被關閉,被阻塞的線程的中斷标志将被設定,并将抛出java.nio.channels.ClosedByInterruptException。
C. 如果線程被阻塞是由于java.nio.channels.Selector時,則被阻塞的線程會立即從選擇操作中傳回,并且可能傳回一個非零值,就像調用java.nio.channels.Selector的wakeup()方法;同時中斷标志将被設定。java.nio.channels.ClosedByInterruptException。
而如果不在上述描述的任一情形下,則線程的中斷标志将被設定。同時中斷一個不存活的線程沒有任何作用。
同樣的,在interrupt()方法的具體實作中是調用native方法interrupt0()實作的,但是該本地方法僅僅隻是設定中斷标志。
在看兩個檢測中斷标志的方法之前,我們先看一個native方法isInterrupted(boolean ClearInterrupted):
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
這個本地方法是用于檢測線程是否被中斷,它有一個布爾類型參數ClearInterrupted,意思是否清除中斷标志。interrupted()方法與isInterrupted()方法都是通過 isInterrupted(boolean ClearInterrupted)實作的。
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
根據靜态方法interrupted()的注釋我們可以了解到該方法時為了檢測目前線程是否被中斷,調用該方法會清除中斷狀态。注釋裡提到如果調用兩次interrupted()方法,則第二次檢測是否被中斷傳回false;除非在兩次interrupted()方法之間在此調用interrupt()方法。在這裡主要需要記住靜态方法會清除中斷标志。
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
根據執行個體rrupted()的注釋我們可以了解到該方法時為了檢測目前線程是否被中斷,但是調用該執行個體方法不會對線程中斷标志有任何影響。在這裡主要需要記住執行個體方法不會清除中斷标志。
下面根據相關demo進一步了解下三個方法的作用:
package com.test;
public class Main {
private static long doTask(long base) {
if (System.currentTimeMillis() - base >= 1000) {
System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getName());
base = System.currentTimeMillis();
}
return base;
}
public static void main(String[] args) throws InterruptedException {
Runnable taskA = () -> {
long base = System.currentTimeMillis();
long base_0 = base;
//不檢查中斷
while (System.currentTimeMillis() - base_0 <= 5000){
base = doTask(base);
}
};
Runnable taskB = () -> {
long base = System.currentTimeMillis();
long base_0 = base;
//采用Thread.interrupted()檢查中斷,中斷政策為列印中斷标志後繼續運作
while (System.currentTimeMillis() - base_0 <= 5000){
base = doTask(base);
if (Thread.interrupted()){
System.out.println(System.currentTimeMillis() + ":Thread " +
Thread.currentThread().getName() + " 中斷标志:" +
Thread.currentThread().isInterrupted());
//調用Thread.currentThread().isInterrupted()列印中斷标志時,列印為false,
//由TaskC的列印知道isInterrupted()不會清除中斷标志,
//則表明中斷标志是在第一次調用Thread.interrupted()時被清除
}
}
};
Runnable taskC = () -> {
long base = System.currentTimeMillis();
long base_0 = base;
//采用Thread.currentThread().isInterrupted()檢查中斷,中斷政策為列印中斷标志後退出
while (System.currentTimeMillis() - base_0 <= 5000){
base = doTask(base);
if (Thread.currentThread().isInterrupted()){
System.out.println(System.currentTimeMillis() + ":Thread " +
Thread.currentThread().getName() + " 中斷标志:" +
Thread.currentThread().isInterrupted());
return;
}
}
};
Thread threadA = new Thread(taskA, "A");
Thread threadB = new Thread(taskB, "B");
Thread threadC = new Thread(taskC, "C");
threadA.start();
threadB.start();
threadC.start();
Thread.sleep(2500);
threadA.interrupt();
threadB.interrupt();
threadC.interrupt();
}
}
以下為demo列印結果:
1563245234861:C
1563245234861:A
1563245234861:B
1563245235861:B
1563245235861:A
1563245235861:C //線程C在中斷後處理完中斷退出
1563245236361:Thread C 中斷标志:true //表明isInterrupted()不會清除中斷标志
1563245236361:Thread B 中斷标志:false //由于上一條列印可知isInterrupted()不會清除中斷标志,
//此處的false則表明中斷标志是在第一次調用Thread.interrupted()時被清除
1563245236861:B //線程B在中斷後處理完中斷就繼續執行任務
1563245236861:A //線程A不理會中斷
1563245237861:B
1563245237861:A
1563245238861:B
1563245238861:A
最後,對于其他線程是否中斷目前線程這個我們管不着,但是線程是否進行中斷,中斷後程式是否繼續運作即中斷政策就是程式員的事了,對待中斷需要謹慎。
2.Thread類API
上文中介紹線程的排程等知識時我們已經了解了部分API,下面将簡要的介紹一下我們還未提到但是比較常用的Thread類API。
關鍵字 | 傳回值類型 | 名稱 | 參數 | 作用 |
---|---|---|---|---|
static native | Thread | currentThread | / | 傳回對目前正在執行的線程對象的引用。 |
synchronized | void | start | / | 使該線程開始執行;Java 虛拟機調用該線程的 run 方法。 |
/ | void | run | / | 如果該線程是使用獨立的 Runnable 運作對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作并傳回。 |
final native | boolean | isAlive | / | 測試線程是否處于活動狀态。 |
final | void | setPriority | int | 更改線程的優先級。 |
final | int | getPriority | / | 傳回線程的優先級。 |
final synchronized | void | setName | String | 改變線程名稱,使之與參數相同。 |
/ | long | getId | / | 傳回該線程的辨別符。 |
final | String | getName | / | 傳回該線程的名稱。 |
final | ThreadGroup | getThreadGroup | / | 傳回該線程所屬的線程組。 |
static | int | activeCount | / | 傳回目前線程的線程組中活動線程的數目。 |
/ | ClassLoader | getContextClassLoader | / | 傳回該線程的上下文 ClassLoader。 |
/ | void | setContextClassLoader | ClassLoader | 設定該線程的上下文 ClassLoader。 |
/ | String | toString | / | 傳回該線程的字元串表示形式,包括線程名稱、優先級和線程組。 |
/ | State | getState | / | 傳回該線程的狀态。 |
部分未提及的API請自行檢視Thread類源碼。
3. 線程生命周期
線上程的排程裡稍微提及了線程的狀态。下面就來看一下線程的整個生命周期是什麼樣子的。
Thread類中定義了一個枚舉内部類State,裡面列舉了線程生命周期中的所有狀态。在這裡首先貼上State的完整API,再進行介紹。
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
Java語言對線程定義了6種狀态,在任意一個時間點,一個線程隻能有且隻有其中一個其中的一種狀态。PS:這是官方定義的,我會根據源碼上的注釋解釋一下,在解釋種我們給出其他的了解。這6種狀态分别如下:
NEW:建立後尚未啟動的線程處于這種狀态。簡單來說,就被new之後尚未執行start()方法的線程。這種狀态沒有什麼需要注意的,記住隻能在這個狀态下設定線程是否為守護線程。
RUNABLE:注釋中提及這種狀态是JVM中runable線程的狀态,但是這個時候線程可能正在等待作業系統資源,例如CPU資源。簡單來說,JVM中為Runable的線程狀态包括了作業系統線程狀态中的Runable狀态(就緒狀态),正在等待着CPU為線程配置設定執行時間;和Running狀态(運作狀态),獲得CPU時間片正在執行。正常情況下我們會把這種狀态分類為Runable和Running,是以大家見到Runable狀态在不提及JVM時我們單指就緒狀态;Running是運作狀态。
BLOCK:先看一下注釋,注釋中提及這個狀态是一個線程正在阻塞等待一個互斥鎖時的狀态,但是在阻塞和等待之間主要需要關注阻塞這個特性。我們需要記住為什麼會阻塞?因為這個線程為了進入同步塊或者同步方法而正在等待一個互斥鎖;或者這個線程從wait方法調用傳回後為了重新進入同步塊或者同步方法時而正在等待一個互斥鎖。記住這個狀态是在另一個線程放棄一個互斥鎖,而這個線程等待獲得互斥鎖進入臨界區域時的狀态。特别要記住等待鎖的線程是BLOCK狀态。
WAITING:這個狀态可以翻譯成無限期等待,與下面要列出的TIMED_WAITING限期等待相對應。處于這種狀态的線程需要被其他線程顯式的喚醒。調用以下方法讓線程進入無限期的等待狀态:
沒有設定Timeout參數的Object.wait()方法;需要其他線程調用notify()或者notifyAll()方法顯式喚醒;
沒有設定Timeout參數的Thread.join()方法;等待目前線程死亡時傳回,上文中提到join()方法内部是通過wait()方法實作的,而線上程死亡時會調用notifyAll()方法。
LockSupport.park()方法;等待其他線程調用unpark()方法喚醒。
TIMED_WAITING:限期等待狀态,處于這種狀态的線程也不會被配置設定CPU執行時間,不過無須等待被其他線程顯式地喚醒,在一定時間之後它們由系統自動喚醒。以下方法會讓線程進入限期等待狀态:
設定了Timeout參數的Object.wait()方法;逾時自動喚醒。
設定了Timeout參數的Thread.join()方法; 逾時自動喚醒。
LockSupport.parkNanos()/parkUntil()方法; 逾時自動喚醒。
Thread.sleep()方法,逾時自動喚醒。
TERMINATED:已終止線程的線程狀态,線程已經結束執行。
不過調用wait()方法後的狀态我們先看以下demo:
public class StateDemo {
private static final Object lockObject = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = new Runnable() {
@Override
public void run() {
synchronized (lockObject){
try {
lockObject.wait(15000);
//逾時自動傳回,線程B自動傳回,沒有競争到鎖,由wait狀态轉換為block狀态
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true){
//doSomeThing
}
}
}
};
Thread a = new Thread(task, "A");
Thread b = new Thread(task, "B");
Thread c = new Thread(task, "C");
a.start();
b.start();
c.start();
Thread.sleep(10000);
synchronized (lockObject){
lockObject.notify();
//喚醒A線程,競争到鎖,由wait狀态轉換為runable狀态,配置設定時間片後可以運作
lockObject.notify();
//喚醒C線程,沒有競争到鎖,由wait狀态轉換為block狀态
}
}
}
我們再通過jvisualvm看wait()方法傳回後的線程狀态:
在這裡可以看出wait()方法傳回時可能會是runable狀态,可能是block狀态,關鍵看這個是否競争到鎖。競争到鎖就是runable狀态,沒有競争到鎖,在等待鎖的過程就是block狀态。
是以整個線程生命周期中線程狀态轉換關系如下:
4. 生産者消費者模式
了解了線程的基本知識,我們使用線程的基本排程手段wait()/notifyAll()/sleep()方法實作簡單的生産者消費者模式。
package com.test.ProducerConsumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class SynchronizedDemo {
//蛋糕類型map
private static final Map<Integer, String> cakeMap = new HashMap<>();
//蛋糕集合,生産者,消費者緩沖區
private final List<String> cakeList = new ArrayList<>();
{
cakeMap.put(1, "奶油蛋糕");
cakeMap.put(2, "水果蛋糕");
cakeMap.put(3, "慕斯蛋糕");
//初始化緩沖區元素
cakeList.add("奶油蛋糕");
cakeList.add("水果蛋糕");
cakeList.add("慕斯蛋糕");
}
//生産者類,最快每隔一秒生産一塊蛋糕
class ProducerTask implements Runnable {
@Override
public void run() {
while (true) {
try {
synchronized (cakeList) {
while (cakeList.size() == 5) {
//蛋糕總數達到5個時,生産者不再生産
System.out.println("蛋糕數目達到最大上限。");
cakeList.wait();
}
String cake = cakeMap.get((int) ((Math.random() * 3)) + 1);
cakeList.add(cake);
System.out.println(System.currentTimeMillis() + ":" +
Thread.currentThread().getName() + "生産:" +
cake + ", 現在蛋糕總數:" + cakeList.size());
cakeList.notifyAll();
}
//生産者最快一秒制作一個
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
class ConsumerTask implements Runnable {
@Override
public void run() {
while (true) {
try {
synchronized (cakeList) {
while (cakeList.size() == 0) {
//蛋糕數目為0時,消費者不再消費蛋糕
System.out.println("蛋糕已售罄。");
cakeList.wait();
}
String cake = cakeList.remove(cakeList.size() - 1);
System.out.println(System.currentTimeMillis() + ":" +
Thread.currentThread().getName() + "消費:" +
cake+ ", 現在蛋糕總數:" + cakeList.size());
cakeList.notifyAll();
}
//每個消費者最快1秒,最慢3秒消費一個
TimeUnit.SECONDS.sleep((int)(Math.random() * 3) + 1);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
private void startProducerConsumer(){
Runnable producerTask = new ProducerTask();
Runnable consumerTask = new ConsumerTask();
Thread producer = new Thread(producerTask, "producer");
Thread consumer_1 = new Thread(consumerTask, "consumer1");
Thread consumer_2 = new Thread(consumerTask, "consumer2");
producer.start();
consumer_1.start();
consumer_2.start();
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.startProducerConsumer();
}
}
運作的結果大體類似如下:
1563454204397:producer生産:水果蛋糕, 現在蛋糕總數:4
1563454204397:consumer1消費:水果蛋糕, 現在蛋糕總數:3
1563454204397:consumer2消費:慕斯蛋糕, 現在蛋糕總數:2
1563454205397:producer生産:奶油蛋糕, 現在蛋糕總數:3
1563454205397:consumer2消費:奶油蛋糕, 現在蛋糕總數:2
1563454206397:producer生産:水果蛋糕, 現在蛋糕總數:3
1563454206397:consumer1消費:水果蛋糕, 現在蛋糕總數:2
1563454207397:producer生産:水果蛋糕, 現在蛋糕總數:3
1563454207397:consumer2消費:水果蛋糕, 現在蛋糕總數:2
1563454208397:producer生産:慕斯蛋糕, 現在蛋糕總數:3
1563454208397:consumer1消費:慕斯蛋糕, 現在蛋糕總數:2
1563454208397:consumer2消費:水果蛋糕, 現在蛋糕總數:1
參考:
《深入了解Java虛拟機》
https://www.cnblogs.com/tiancai/p/9371655.html
https://www.cnblogs.com/qingquanzi/p/8228422.html
https://www.jianshu.com/p/f4454164c017
http://hg.openjdk.java.net/jdk8u
http://docs.oracle.com/javase/6/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html
https://www.cnblogs.com/luochengor/archive/2011/08/11/2134818.html
https://my.oschina.net/payzheng/blog/692635