Java核心技術卷一 8. java並發
什麽是線程
每個進程擁有自己的一整套變量,而線程則共享數據。
沒有使用多線程的程序,調用 Thread.sleep 不會創建一個新線程,用於暫停當前線程的活動。程序未結束前無法與程序進行交互。
使用線程給其他任務提供機會
將代碼放置在一個獨立的線程中,事件調度線程會關註事件,並處理用戶的動作。
在一個單獨的線程中執行一個任務的簡單過程:
將任務代碼移到實現了 Runnable 接口的類的 run 方法中。
public interface Runnable{ void run(); } Runnable r = () -> { task code; };
由 Runnable 創建一個 Thread 對象:
Thread t = new Thread(r);
啟動線程:
t.start();//啟動這個線程,將引發調用 run() 方法。新程序將並發運行。
中斷線程
線程的 run 方法執行方法體重最後一條語句後,並由執行 return 語句返回時,或者在方法中沒有捕獲異常,程序終止。
java 早期有一個 stop 方法,可以終止線程,目前已被棄用。
沒有可以強制線程終止的方法。然而,interrupt 方法可以用來請求終止線程。
線程調用 interrupt 方法時,線程的中斷狀態將被置位。這是每一個線程都具有 boolean 標誌。每個線程不時的檢查這個標誌,判斷線程是否被中斷。
判斷線程是否被置位,currentThread 方法獲得當前線程,isInterrupted 方法判斷此線程是否被置位:
Thread.currentThread().isInterrupted()
在一個被阻塞的線程(調用 sleep 或 wait )上調用 interrupt 方法時,阻塞調用將會被 Interrupted Exception 異常中斷。
中斷一個線程不會讓程序終止,不過是引起它的註意。被中斷的線程可以決定如何響應中斷。一般線程將簡單地將中斷作為一個終止的請求。
如果在中斷狀態被置位時調用 sleep 方法,它不會休眠。它會清楚狀態拋出 InterruptedException
。
異常有兩種合理的選擇:
- 在 catch 子句中調用
Thread.currentThread().interrupt()
- 用
throws InterruptedException
標記你的方法,調用者可以捕獲這個異常。
//java.lang.Thread
void interrupt() //發送中斷線程請求。中斷狀態為 true。如果線程 sleep 拋出異常。
static boolean interrupted() //測試線程是否中斷。靜態方法副作用,中斷狀態重置為 false
boolean isInterrupted() //測試線程是否被終止。
static Thread currentThread() //當前執行線程的 Thread 對象
線程狀態
有 6 種狀態:
- New(新創建)
- Runnable(可運行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed waiting(計時等待)
- Terminated(被終止)
使用 getState 方法確定線程的狀態。
新創建線程 New
當用 new 操作符創建一個新線程時,如 new Thread(r)
,該線程還沒有開始運行。
可運行線程 Runnable
一旦調用 start 方法,線程處於 runnable 狀態。
一個可運行的選擇可能正在運行也可能沒有運行。
搶占式調度系統給每一個可運行線程一個時間片來執行任務。當時間片用完,操作系統剝奪該線程的運行權,並給另一個線程運行機會。
被阻塞線程和等待線程 Blocked Waiting
這個狀態不允許任何代碼且消耗最小的資源。知道線程調度器重新激活它。細節取決於它是怎樣達到非活動狀態的。
- 當一個線程視圖獲取一個內部的對象鎖,而該鎖被其他線程持有,進入阻塞狀態。其他線程釋放該鎖,並且線程調度器運行本線程持有它的時候,該線程將變為非阻塞狀態。
- 當線程等待另一個線程通知調度器一個條件時,他自己進入等待狀態。在調用
Object.wait
或Thread.join
方法,或者等待java.util.concurrent
中的Lock
或Condition
時,就會出現這種情況。被阻塞狀態與等待狀態是有很大不同的。 - 調用一些方法將導致線程進入計時等待狀態。這一狀態一直保持到超時期滿或者接受到適當的通知。帶有超時參數的方法有
Thread.sleep
和Object.wait
、Thread.join
、Lock.tryLock
以及Condition.await
的計時版。
被終止的線程 Terminated
兩種原因被終止:
- 因為 run 方法正常退出而自然死亡
- 沒有捕獲的異常終止了 run 方法意外死亡
線程屬性
線程優先級
java 程序中,每個線程有一個優先級。默認,一個線程繼承它的父線程的優先級。
可以用 setPriority 方法提高或降低任何一個線程的優先級。(1~10)
使用 static void yield 方法會導致當前執行線程處於讓步狀態,有其他的可運行線程具有至少與此線程同樣高的優先級,那麽這些線程接下來會被調度。
守護線程
通過調用 t.setDaemon(true);
將線程裝換為守護線程。
唯一用途為其他線程提供服務。只剩下守護線程時,虛擬機就退出了。如計時線程,定時給其他線程發送信號。
守護線程因該永遠不要去訪問固有資源,如文件、數據庫。因為它會隨時中斷。
未捕獲異常處理器
線程的 run 方法不能拋出任何受查異常,非受查異常會導致線程終止。此時線程會死亡,死亡之前異常被傳遞到一個用於未捕獲異常的處理器。
處理器必須處於一個實現 Thread.UncaughtExceptionHandler
接口的類。類有一個方法void uncaughtException(Thread t, Throwable e)
可以用setUncaughtExceptionHandler
方法為任何線程安裝一個處理器。
用靜態方法 Thread.setDefaultUncaughtExceptionHandler
為所有線程安裝一個默認的處理器。
不為獨立的線程安裝處理器,此時的處理器就是該線程的 ThreadGroup
對象。
線程組
線程組是一個統一管理的線程集合。默認下,創建的所有線程屬於相同的線程組。
ThreadGroup 類實現Thread.UncaughtExceptionHandler
接口。它的方法uncaughtException
操作如下:
- 如果該線程有父線程組,那麽父線程組的
uncaughtException
方法被調用。 - 否則,如果
Thread.getDefaultExceptionHandler
方法返回一個非空的處理器,則調用該處理器。 - 否則,如果
Throwable
是ThreadDeath
的一個實例,什麽都不做。 - 否則,線程的名字以及
Throwable
的棧軌跡被輸出到System.err
上。
同步
兩個或兩個以上的線程需要共享同一數據的存取。如果不使用同步,線程都調用了一個修改對象狀態的方法,它們會產生訛誤的對象。這一情況為競爭條件。
競爭條件詳解
當兩個線程視圖同時更新同一個賬戶的時候,會出現一些問題,假設他們同時執行指令:
accounts[to] += amout;
處理過程:
- 將
accouts[to]
加載到寄存器。 - 增加
amount
。 - 將結果寫回
accounts[to]
。
如果第 1 個線程執行完步驟1和2後,被剝奪了運行權。第 2 個線程被喚醒並修改了 accounts 數組中的同一項。然後,第 1 個線程被喚醒並完成其第 3 步。
這一動作讓線程 1 擦除了線程 2 所做的操作。總金額不在正確。
線程1 | 線程2 | 寄存器 accouts[to] | accouts[to] |
---|---|---|---|
加載 accouts[to] | 線程1 10 | 10 | |
增加 amout | 線程1 12 | 10 | |
加載 accouts[to] | 線程2 10 | 10 | |
增加 amout | 線程2 12 | 10 | |
寫入 accouts[to] | 線程2寫入 12 | 12 | |
寫入 accouts[to] | 線程1寫入 12 | 12 |
如果能夠確保線程在失去控制之前方法運行完成,那麽銀行賬戶對象的狀態永遠不會出現訛誤。
鎖對象 ReentrantLock 重入鎖
有兩種機制防止代碼塊受並發訪問的幹擾:
- synchronized 關鍵字
- java SE 5.0 引入 ReentrantLock 類
synchronized 關鍵字自動提供一個鎖以及相關的“條件”,對於大多數小顯示鎖的情況。
java.util.concurrent 框架為這些基礎機制提供獨立的類。
用 ReentrantLock 保護代碼的基本結構如下:
private Lock bankLock = new ReentrantLock();
bankLock.lock();
try{
critical section
} finally {
bankLock.unlock();
}
這個結構能夠確保任何時候只有一個線程進入臨界區。一旦一個程序使用 lock 方法封鎖了對象,其他任何程序都無法通過 lock 語句。當其他線程調用 lock 時,它們被阻塞,直到第一個線程釋放鎖對象。
鎖是可重入的,線程可以重復地獲取已經持有的鎖。鎖保持一個持有計數來跟蹤對 lock 方法的嵌套調用,每進入一次計數器加1。線程在每一次調用 lock 都要調用 unlock 來釋放鎖。由於這一特性,被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法。
ReentrantLock 重入鎖,還可以構建一個帶有公平策略的鎖。
條件對象 Condition
使用條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程。條件對象經常被稱為條件變量。
一個鎖對象可以有一個或多個相關的條件對象。用 newCondition 方法獲得一個條件對象。
class Bank{
private Condition sufficientFunds;
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}
//發現 transfer 方法余額不足,調用
sufficientFunds.await();
調用 await 方法的線程和等待獲得鎖的線程存在本質上的不同。
使用 await 方法的線程進入該條件的等待集,當鎖可用時,不會馬上解除阻塞,知道另一個線程調用同一條件上的 signalAll 方法時為止。
當另一個線程做完操作,應調用:
sufficientFunds.singnalAll();
此時重新激活因為這一條件而等待的所有線程。從阻塞的地方繼續執行。
一個線程調用了 await 方法,而別的程序沒有調用 signalAll 方法,那麽就是死鎖,永遠阻塞。
signal 方法可以隨機解除一個線程的阻塞狀態。但有如果再次阻塞,並沒有其他線程再次調用 singnal ,線程就會死鎖。
synchronized 關鍵字
鎖和條件的關鍵之處:
- 鎖用來保護代碼片段,任何時刻只能有一個線程執行被保護的代碼。
- 鎖可以管理視圖進入被保護代碼段的線程。
- 鎖可以擁有一個或多個相關的條件對象。
- 每個條件對象管理那些已經進入保護的代碼段但不能運行的程序。
java 每一個對象都有一個內部鎖,如果一個方法用 synchronized 關鍵字聲明,那麽對象的鎖將保護整個方法。當調用該方法時,線程必須獲得內部的對象鎖。
public synchronized void method(){
method body
}
//等價
public void method(){
this.intrinsicLock.lock();
try {
method body
} finally {
this.intrinsicLock.unlock();
}
}
synchronized 內部對象鎖只有一個相關條件。
- wait 方法添加一個線程到等待集中。
- notifyAll / notify 方法解除等待線程的阻塞狀態。
- 他們是 Object 類的 final 方法。
與下面的方法等價:
intrinsicLock.await();
intrinsicLock.signallAll();
intrinsicLock.signall();
內部鎖存在一些局限:
- 不能中斷一個正在試圖獲得鎖的線程。
- 試圖獲得鎖時不能設定超時。
- 每個鎖僅有單一的條件,可能是不夠的。
使用哪種鎖的建議:
- 最好既不是用 Lock / Condition 也不使用 synchronized 關鍵字。許多情況下你可以使用 java.util.concurrent 包中的一種機制,它會為處理所有的加鎖。
- 如果 synchronized 適合程序,盡量使用它,它可以減少編寫的代碼數量。
- 特別需要 Lock / Condition 結構提供的獨有特性時,才使用 Lock / Condition。
同步阻塞 synchronized(obj){...}
線程可以通過同步方法獲得鎖。通過進入一個同步阻塞也可以獲得鎖:
synchronized(obj){
critical section
}//獲得 obj 的鎖
lock 對象被創建僅僅是用來使用每個 Java 對象持有的鎖。
有時程序員使用一個對象的鎖來實現額外的原子性,實際上稱為客戶端鎮定。
監視器概念
鎖和條件是線程同步的強大工具,但它們不是面向對象的。
監視器可以實現在不加鎖的情況下,保證多線程的安全性。
監視器特性:
- 監視器只包含私有域的類。
- 每個監視器類的對象有一個相關的鎖。
- 使用該鎖對所有的方法進行加鎖。
- 該鎖可以有任意多個相關條件。
如果一個方法用 synchronized 關鍵字聲明,那麽,它表現的就像是一個監視器方法。
三個方法 java 對象不同於監視器,安全性下降:
- 域不要求必須是 private。
- 方法不要求必須是 synchronized。
- 內部鎖對客戶是可用的。
Volatile 域
如果向一個變量寫入值,而這個變量接下來可能會被另一個線程讀取,或者,從一個變量讀取,而這個變量可能是之前被另一個線程寫入的,此時必須使用同步。
volatile 關鍵字為實例域的同步訪問提供了一種免鎖機制。
如果一個域為 volatile ,那麽編譯器和虛擬機就知道該域是可能被另一個線程並發更新的。
private volatile boolean done;
public boolean isDone(){ return done; }
public void setDone(){ done = true; }
Volatile 不能提供原子性。不能確保讀取、翻轉和寫入不被中斷。
done = !done
final 變量
使用 final 的共享域可以安全地訪問一個共享域。
final Map<String, Double> accounts = new HashMap<>();
線程會在構造函數完成構造只有才能看到這個 accounts 變量。
不使用 final ,不能保證其他線程看到更新後的值,可以只是看 null,而不是新構造的 HashMap。
原子性
java.util.concurrent.atomic
包中有很多類使用了高效的機器級指令來保證其他操作的原子性。
死鎖
有可能會因為每一個線程要等待更多的錢款存入而導致所有線程都被阻塞。這樣的狀態成為死鎖。
線程局部變量
避免共享變量,使用 ThreadLocal 輔助類為各個線程提供各自的實例。
鎖測試與超時
謹慎的申請鎖:
if(myLock.tryLock()){
try {
...
} finally {
myLock.unlock();
}
} else {
do something else
}
可以調用 tryLock 時,使用超市參數:
if (myLock.tryLock(100, TimeUnit.MILLISECONDS)) ...
TimeUnit 是一個枚舉類型,超時參數可以讓在等待期間被中斷的線程拋出異常。允許程序打破死鎖。
讀/寫鎖
java.util.concurrent.locks 包定義了兩個鎖類。
讀/寫鎖的必要步驟:
構造一個 ReentrantReadWriteLock 對象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
抽取讀鎖和寫鎖
private Lock readLock = rwl.readLock();
private Lock readLock = rwl.writeLock();
對所有的獲取方法加讀鎖
public double getTotalBalance(){ readLock.lock(); try {...} finally { readLock.unlock(); } }
對所有的修改方法加寫鎖
public void transfer(...){ writeLock.lock(); try {...} finally { writeLock.unlock();} }
為什麽棄用 stop 和 suspend 方法
stop 方法天生就不安全。
suspend 方法會經常導致死鎖。
阻塞隊列
當視圖向隊列添加元素而隊列已滿,或是想從隊列移出元素而隊列為空的時候,阻塞隊列導致線程阻塞。
線程安全的集合
高效的映射、集和隊列
java.util.concurrent 包提供了映射、 有序集和隊列的高效實現:
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- ConcurrentLinkedQueue
這些集合使用復雜的算法,通過允許並發地訪問數據結構的不同部分來使競爭極小化。
集合返回弱一致性的叠代器。叠代器不一定能反映出它們被構造之後的所有的修改,但是,它們不會將同一個值返回兩次,也不會拋出 ConcurrentModificationException 異常。
ConcurrentHashMap 默認支持多達 16 個寫著線程同時執行。如果同一時間寫著超過 16 ,其他線程將會暫時阻塞。可以指定更大數目的構造器。
映射條目的原子更新
ConcurrentHashMap 只有為數不多的方法可以實現原子更新。
以下不是線程安全的:
Long oldValue = map.get(word);
Long newValue = oldValue == null ? 1 : oldValue + 1;
map.put(word, newValue);//Error 可能不會替換 oldValue
傳統方法使用 replace 操作,它會以原子方式用一個新值替換原值,前提是之前沒有其它線程把原值替換為其他值。
do {
oldValue = map.get(word);
newValue = oldValue == null ? 1 : oldValue + 1;
}while(!map.replace(word, oldValue, newValue));
或使用ConcurrentHashMap<String, AtomicLong>
或在 java SE 8 中使用ConcurrentHashMap<String, LongAdder>
:
map.putIfAbsent(word, new LongAdder());
map.get(word).increment();
map 函數接受鍵和相關聯的值,它會計算新值。
map.compute(word, (k, v) -> v == null ? 1 : v + 1);
ConcurrentHashMap 中不允許有 null 值。
對並發散列映射的批操作 ConcurrentHashMap
- 搜索為每個鍵或值提供一個函數,直到函數生成一個非 null 的結果。然後搜索終止,返回這個函數的結果。
- 歸約組合所有鍵或值,使用鎖提供的一個累加函數。
- forEach為所有鍵或值提供一個函數。
每個操作都有 4 個版本:
- operationKeys:處理鍵
- operationValues:處理值
- operation:處理鍵或值
- operationEntries:處理 Map.Entry 對象
並發集視圖
沒有ConcurrentHashSet 類,但你可以使用 ConcurrentHashMap 創建視圖。
Set<String> words = ConcurrentHashMap.<String>newKeySet();
//keySet 方法包含一個默認值,在為集增加元素時使用
Set<String> words = map.keySet(1L);
words.add("Java");
寫數組的拷貝
CopyOnWriteArrayList 和 CopyOnWriteArraySet 是線程安全的集合,所有的修改線程對底層數組進行復制。
並行數組算法
靜態 Arrays.parallelSort 方法可以對 一個基本類型值或對象的數組排序。
parallelPrefix 方法,它會用對應一個給定結合操作的前綴的累加結果替換 各個數組元素。
較早的線程安全集合
Vector 和 Hashtable 類就提供了線程安全的動態數組和散列表的 實現。
任何集合類都可以通過使用同步包裝器(Collections .synchronized*)變成線程安全的
如果在另一個線程可能進行修改時要對集合進行叠代,仍然需要使用“ 客戶端” 鎖定:
synchronized (synchHashMap) {
Iterator iter = synchHashMap.keySet().iterator();
while (iter.hasNextO) . .
}
最好使用 java.util.conncurrent
包中定義的集合, 不使用同步包裝器中的。特別是, 假如它 們訪問的是不同的桶, 由於 ConcurrentHashMap
已經精心地實現了,多線程可以訪問它而且 不會彼此阻塞。
有一個例外是經常被修改的數組列表。在那種情況下,同步的 ArrayList
可 以勝過 CopyOnWriteArrayList
Callable 與 Future
Runnable 封裝一個異步運行的任務,可以把它想象成為一個沒有參數和返回值的異步方法。
Callable 與 Runnable 類似,但是有返回值。Callable 接口是一個參數化的類型, 只有一個方法 call。
Future 保存異步計算的結果。可以啟動一個計算,將 Future 對象交給某個線程,然後忘掉它。Future 對象的所有者在結果計算好之後就可以獲得它。
get 方法的調用被阻塞, 直到計算完成。如果在計算完成之前, 第二個方法的調 用超時,拋出一個 TimeoutException 異常。如果運行該計算的線程被中斷,兩個方法都將拋 出 IntermptedException。如果計算已經完成, 那麽 get 方法立即返回。
如果計算還在進行,isDone 方法返回 false 如果完成了, 則返回 true。
可以用 cancel 方法取消該計算。如果計算還沒有開始,它被取消且不再開始。如果計算 處於運行之中,那麽如果 maylnterrupt 參數為 true, 它就被中斷。
執行器
如果程序中創建了大 量的生命期很短的線程,應該使用線程池 。
一個線程池中包含許多準備運行的 空閑線程。將 Runnable 對象交給線程池, 就會有一個線程調用 run 方法。 當 run 方法退出 時,線程不會死亡,而是在池中準備為下一個請求提供服務。
另一個使用線程池的理由是減少並發線程的數目。創建大量線程會大大降低性能甚至使 虛擬機崩潰。如果有一個會創建許多線程的算法, 應該使用一個線程數“ 固定的” 線程池以 限制並發線程的總數。
執行器( Executor) 類有許多靜態工廠方法用來構建線程池。
- newCachedThreadPool 必要時創建新線程;空閑線程會被保留 60 秒
- newFixedThreadPool 該池包含固定數量的線程;空閑線程會一直被保留
- newSingleThreadExecutor 只有一個線程的 “ 池”, 該線程順序執行每一個提交的任務(類似於 Swing 事件分配線程)
- newScheduledThreadPool 用於預定執行而構建的固定線程池, 替代 java.util.Timer
- newSingleThreadScheduledExecutor 用於預定執行而構建的單線程 “ 池”
線程池
newCachedThreadPool
newFixedThreadPool
newSingleThreadExecutor
這 3 個方法返回實現了 ExecutorService 接口的 ThreadPoolExecutor 類的對象。
可用下面的方法之一將一個 Runnable 對象或 Callable 對象提交給 ExecutorService:
Future submit(Runnable task)
Future submit(Runnable task, T result)
Future submit(Callable task)
該池會在方便的時候盡早執行提交的任務。
調用 submit 時,會得到一個 Future 對象,可 用來查詢該任務的狀態。
- 第一個 submit 方法返回一個奇怪樣子的 Future。可以使用這樣一個對象來調用 isDone、 cancel 或 isCancelled。但是, get 方法在完成的時候只是簡單地返回 null。
- 第二個版本的 Submit 也提交一個 Runnable, 並且 Future 的 get 方法在完成的時候返回指 定的 result 對象。
- 第三個版本的 Submit 提交一個 Callable, 並且返回的 Future 對象將在計算結果準備好的 時候得到它。
調用 shutdown 方法啟動該池的關閉序列。被關閉的執 行器不再接受新的任務。當所有任務都完成以後,線程池中的線程死亡。
調用 shutdownNow 方法,該池取消尚未開始的所有任務並試圖中斷正在運行的線程。
下面總結了在使用連接池時應該做的事:
- 調用 Executors 類中靜態的方法 newCachedThreadPool 或 newFixedThreadPool。
- 調用 submit 提交 Runnable 或 Callable 對象。
- 如果想要取消一個任務, 或如果提交 Callable 對象, 那就要保存好返回的 Future 對象。
- 當不再提交任何任務時,調用 shutdown。
預定執行
ScheduledExecutorService 接口具有為預定執行或重復執 行任務而設計的方法。 後兩個方法實現了這個接口。
控制任務組
invokeAll 方法提交所有對象到一個 Callable 對象的集合中,並返回一個 Future 對象的列 表,代表所有任務的解決方案。
Fork-Join 框架
有些應用使用了大量線程, 但其中大多數都是空閑的。舉例來說, 一個 Web 服務器可能會為每個連接分別使用一個線程。另外一些應用可能對每個處理器內核分別使用一個線程, 來完成計算密集型任務, 如圖像或視頻處理。Java SE 7中新引入了 fork-join 框架,專門用來 支持後一類應用。假設有一個處理任務, 它可以很自然地分解為子任務。
if (problemSize < threshold)
solve problem directly
else {
break problem into subproblems
recursively solve each subproblem
combine the results
}
可完成 Futrue
處理非阻塞調用的傳統方法是使用事件處理器, 程序員為任務完成之後要出現的動作註冊一個處理器。
同步器
java.util.concurrent 包包含了管理相互合作的線程集的類 。
信號量
一個信號量管理許多的許可證(permit)。
倒計時門栓
一個倒計時門栓( CountDownLatch) 讓一個線程集等待直到計數變為 0。倒計時門栓是 一次性的。一旦計數為 0, 就不能再重用了。
障柵
CyclicBarrier 類實現了一個集結點(rendezvous) 稱為障柵( barrier)。考慮大量線程運行 在一次計算的不同部分的情形。當所有部分都準備好時,需要把結果組合在一起。當一個線 程完成了它的那部分任務後, 我們讓它運行到障柵處。一旦所有的線程都到達了這個障柵, 障柵就撤銷, 線程就可以繼續運行。
交換器
當兩個線程在同一個數據緩沖區的兩個實例上工作的時候, 就可以使用交換器 ( Exchanger) 典型的情況是, 一個線程向緩沖區填人數據, 另一個線程消耗這些數據。當它 們都完成以後,相互交換緩沖區。
同步隊列
同步隊列是一種將生產者與消費者線程配對的機制。當一個線程調用 SynchronousQueue 的 put 方法時,它會阻塞直到另一個線程調用 take 方法為止,反之亦然。與 Exchanger 的情 況不同, 數據僅僅沿一個方向傳遞,從生產者到消費者。 即使 SynchronousQueue 類實現了 BlockingQueue 接口, 概念上講,它依然不是一個隊 列。它沒有包含任何元素,它的 size方法總是返回 0。
Java核心技術卷一 8. java並發