1. 程式人生 > >java核心思想

java核心思想

第一章 多執行緒

多執行緒和多程序之間的區別:
  本質區別在於每個程序有他自己的變數的完備集,執行緒則共享相同的資料,這個聽起來似乎有些危險,事實上也的確如此,你將會在本章後面的內容中看到這個問題,儘管如此,對於程式來說,共享的變數使執行緒之間的通訊
比程序間的通訊更加有效簡單,而且,對於某些作業系統而言,執行緒比程序更加輕量級。建立和銷燬單個執行緒比發起程序的開銷要小很多。

執行緒優先順序
  在Java程式設計語言中,每一個執行緒都有一個優先順序,預設情況下,一個執行緒繼承他的父執行緒的優先順序,一個執行緒的父執行緒就是啟動他的那個執行緒,你可以用setPriority方法提高或降低任何一個執行緒的優先順序,你可以將優先順序設定為
1-10之間的任何值。預設的優先順序是5.
  無論何時,當執行緒呼叫器有機會去挑選一個新的執行緒時,他會優先考慮高優先順序的執行緒,但是,執行緒優先順序是高度依賴系統的。
  因此最好僅將執行緒優先順序看做執行緒排程器的一個參考因素,千萬不要將程式構建為其功能的正確性依賴於優先順序。
  
守護執行緒
  你可以通過呼叫t.setDaemon(true),將執行緒轉變成一個守護執行緒,這樣的執行緒並沒有什麼神奇的地方,一個守護執行緒的唯一作用就是為其他的執行緒提供服務,計時器執行緒就是一個例子,他定是傳送訊號給其他執行緒,當只剩下守護執行緒時,
虛擬機器就退出了,因為如果剩下的執行緒都是守護執行緒,就沒有繼續執行程式的必要了,
 

執行緒組
  Java程式設計語言允許你建立執行緒組,這樣就可以同時對一組執行緒進行操作。
你可以通過下面的構造器器來建立執行緒組:
  String groupName = "";
  ThreadGroup g = new THreadGroup(groupName);
  ThreadGroup構造器中的字串是用來表示該執行緒組的,他必須是唯一的,然後你可以
新增執行緒到這個組中,方法是線上程的構造器中指定執行緒組。
  Thread t = new Thread(g,threadName);
  要查明某個特定的執行緒組是否有執行緒仍然處於可執行狀態,應該使用activeCount方法。
  if(g.actviceCount() == 0){
  }
  要中斷一個執行緒組中的所有執行緒,只需要在組物件上呼叫interrupt()方法。
  g.interrupt()
  
  
未捕獲異常處理器
  執行緒的run方法不能丟擲任何被檢查的異常,但是不被檢查的異常可以導致執行緒終止,如果楚翔
這種情況,執行緒就是死亡了
  然而,並不需要任何catch子句來處理可以被傳播的異常,線上程死亡錢,異常將被傳播給未捕獲異常的處理器來處理。
處理器必須實現Thread.UncaughtExceptionHandler介面的類,這個介面只有一個方法.


鎖物件
  有兩種機制來保護程式碼塊不受並行訪問的干擾。舊版本的java使用synchronized關鍵字來達到這個目的,而jdk1.5
引進ReentrantLock類,synchronized關鍵字自動提供了一個鎖和相關聯的條件,
  用ReentrantLock保護程式碼塊的基本結構是:
  myLock.lock();
  try{
  }finally{
   myLock.unlock();
  }
  這個結構保證了在任何時刻只有一個執行緒能夠進入臨界區,一旦一個執行緒鎖住了鎖物件,其他任何執行緒都無法通過lock語句,
當其他執行緒呼叫lock時,他們會被阻塞,直到佔用執行緒釋放鎖物件。
  注意每一個bank物件都有個他自己的鎖物件,如果兩個執行緒試圖訪問同一個物件,鎖就會序列的服務於訪問,但是如果兩個執行緒訪問不同的bank物件,
那麼每一個執行緒都會得到一個不同的鎖,兩者都不會阻塞,這正是我們期待的結果,因為當執行緒操作不同的bank例項,彼此之間不會互相影響,
  鎖是可重入的,因為執行緒能夠重複的獲取他已經擁有的鎖,鎖物件維護一個持有計數來追蹤lock方法的巢狀呼叫,執行緒在每次呼叫Lock後都要呼叫unlock
來釋放所,由於這個特性,被一個鎖保護的程式碼可以呼叫另一個使用相同鎖的方法。
  一般而言,你希望保護那些需要多個操作才能更新檢查一個數據結構的程式碼塊,你必須確保這些操作完成之後其他執行緒才可以使用相同的物件
  

條件物件
  通常,一個執行緒進入臨界區,卻發現他必須等待某個條件滿足後才能執行,你要使用一個條件物件來管理那些已經獲得鎖卻不能開始執行有用的工作的執行緒。
讓我們來精化那個銀行模擬程式,我們不希望選擇餘額不足以進行轉賬的賬戶作為轉出賬戶,注意,我們不能用下面這樣的程式碼。
  if(bank.getBalance(from)>=amount)
   bank.transfer(from,to,amount)
  當前執行緒完全有可能在條件語句檢查成功之後,並在transfer被呼叫之前被中斷。
  線上程再次執行前,賬戶餘額可能已經小於要提出的金額,你必須確保在檢查餘額操作
和後面的轉賬操作之間執行緒不會修改餘額,將檢查和轉賬動作同一個鎖保護起來就可以達到目的:


synchronized關鍵字
  在前一節,你看到了如何使用lock和condition物件,在進一步深入之前,我們總結一下lock和condition的關鍵點:
  1鎖用來保護程式碼片段,任何時刻只允許一個執行緒執行被保護的程式碼。
  2鎖可以管理試圖進入被保護程式碼段的執行緒。
  3鎖可以有一個或多個相關的條件物件。
  4每個條件物件管理那些已經進入被保護程式碼段但還不能執行的執行緒
  可以看到使用synchronized關鍵字來編寫程式碼要簡潔的多,當然為了理解程式碼,你必須知道每個物件都有一個隱式的鎖,
並且每一個鎖都有一個隱式條件,由鎖來管理試圖進入synchronized方法的執行緒,而由條件來管理那些呼叫的wait的執行緒。
但是,隱式的鎖和條件存在一些缺點,其中包括:
  你不能終端一個正在試圖獲得鎖的執行緒
  試圖獲得鎖時你不能設定超時
  每個鎖只有一個條件有時候顯得不夠用,
  虛擬機器的加鎖原句不能很好的對映到硬體可用的最有效的加鎖機制上。
 你在程式碼中應該使用哪一種呢?
 最好既不要使用lock,也不要使用synchronized關鍵字,你可以使用java.util.concurrent包中的一種機制
他會為你處理所有的枷鎖。
 如果synchronized關鍵字在你的程式中可以工作,那麼請儘量使用它,這樣可以減少你的程式碼數量,減少出錯的機率
 只有在你非常需要lock結構的獨有特性的時候才能使用他們
 
 void notifyAll()
   解除在該物件上呼叫wait的執行緒的阻塞狀態,這個方法只能在同步方法或同步塊內部呼叫,如果當前執行緒不是物件
 鎖的持有者,該方法丟擲一個IllegaMonitorStateException異常。
 
 void notify()
   隨機選擇一個在該物件上呼叫wait的執行緒,解除他的阻塞狀態,這個方法只能在一個同步方法或同步塊內部呼叫,如果
 當前執行緒不是物件鎖的持有者,該方法丟擲一個IllegaMonitorStateException異常。
 
 void wait(long &&)
   導致執行緒進入等待狀態直到被通知,這個方法只能在一個同步的方法中呼叫,如果當前執行緒不是物件鎖的持有者,該方法丟擲一個IllegaMonitorStateException異常。
   
 
 監視器
 鎖和條件是執行緒同步的強大工具,但他們不是嚴格意義上的面向物件。很多年來,研究院一直在尋找一種方法,可以在不需要程式設計師考慮具體如何加鎖的情況下保證多執行緒的安全
 其中最成功的解決方案是監視器的概念。在java中,一個監視器有一下的特徵:
   一個監視器是隻有一個私有域的類。
   每個監視器類的物件都有一個相關的鎖。
   這個鎖負責對所有方法枷鎖
   這個鎖可以有任意多個關聯條件
   
 
 同步塊
 如果你在處理遺留程式碼,你需要知道內建的同步原句,別忘了每一個物件都有一個鎖,執行緒通過兩種方法來獲得
 鎖,呼叫一個同步方法或者進入同步塊,如果執行緒呼叫obj.method,就需要獲得obj的鎖,如果執行緒以如下方法
 進入一個塊,情況也類似。然後執行緒獲得obj的鎖,鎖可以重入,如果一個執行緒已經獲得了鎖,他可以再次獲得他,
 同時會增加鎖的持有計數,特殊情況是一個同步方法能夠用相同的隱式引數呼叫其他的同步方法,而不需要等待鎖釋放
   你經常會看到在遺留程式碼中非正規的鎖,例如,
   class Bank{
     public void transfer(int from ,int to,int amount){
       synchronized(lock){
        accounts[from]-=amount;
        accounts[to]+=amount;
       }
     private double accounts[];
     private Object lock = new Object();
   }
  這裡,建立lock物件僅是為了使用每一個java物件擁有的鎖,將靜態方法宣告為同步是合法的,如果這個方法被呼叫,他會獲得所關聯的類物件的鎖
  例如如果bank物件有一個同步方法,那麼他被呼叫時,bank.class物件的鎖就會被鎖住
  
  
Volatile域
  有時候,只是為了讀寫例項的一兩個域就是用同步,其帶來的開銷似乎太大了,畢竟,這麼簡單的操作能出什麼錯呢?遺憾的是,
 使用現代的處理器和編譯器,可能出現錯誤的地方有很多。
   1多處理器的計算機能夠暫時在暫存器或本地記憶體緩衝區中儲存記憶體中的值,這麼做鎖造成的結果就是在不同的處理器上執行的執行緒可能在同一個記憶體地址上看到不同的值。
   2編譯器能夠改變指令執行的順序以便吞吐量最大化,這種順序上的變化不會改變程式碼的語義,但編譯器假設只有在程式碼中存在顯式的修改指令時,記憶體中的值才會發生變化,但是,記憶體的值可能會被另一個執行緒所改變
  如果你使用鎖來保護可以被多個執行緒訪問的程式碼,那麼你不需要考慮這些問題,編譯器被要求在必要時重新整理本地快取並且不能不正當的重排序指令,通過這種方法保證不對加鎖的效果產生干擾。
   
   
   Volatile關鍵字為對一個例項的域的同步訪問提供了一個免鎖機制,如果你把一個域宣告為Volatile,那麼編譯器和虛擬機器就知道該域可能會被另一個執行緒併發更新。
  假如一個物件有個布林標記,由一個執行緒設定他的值,而由另一個執行緒來查詢,那麼有下面兩種方法:
   1.使用鎖
     public synchronized boolean isDone(){ return done;}
     private boolean done;
   2.將域宣告為Volatile
     public boolean isDone(){return done;}
     private Volatile boolean done;
  當然訪問一個Volatile變數比訪問一個一般變數要慢,這是為執行緒安全所付出的代價。
  
  總之,在下面三個條件下,對一個域的並行訪問是安全的。
  域是Volatile的。
  域是final的,並且在構造器呼叫完成後被訪問。
  對域的訪問有鎖的保護。
  
  
死鎖
  
  
公平
  在你構建一個ReentrantLock時,你可以制定你需要的一個公平鎖策略
   Lock fairLock = new ReentrantLock(true);
   
鎖測試和超時
  執行緒在呼叫Lock方法來獲得另一個執行緒所持有的鎖時,不時地會發生阻塞,你應該對獲得鎖更加謹慎,tryLock方法試圖獲得一個鎖,如果成功就返回true,否則返回false,並且執行緒可以立即離開去幹任何其他的事。
    if(lock.tryLock()){
      try{}
      finally{myLock.unlock();}
    else
    
  你可以在呼叫時給tryLock方法一個超時引數,如下所示,
   if(lock.tryLock(100,TimeUnit.MILLISECONDS)
  這些方法處理公平和執行緒終端的方式存在細微的差別。
  帶一個超時引數的trylock方法在處理公平性時和lock方法是一樣的,但對於不帶超時的trylock方法,如果它被呼叫時鎖可以
 獲得,那麼當前執行緒就會獲得鎖,而不管是否有執行緒已經等待很舊,如果你不想發生這樣的事情,可以總是呼叫
   if(lock.tryLock(0,TimeUnit.MILLISECONDS)
  lock方法不能被中斷,如果執行緒在等待獲得一個鎖時被中斷,那麼中斷執行緒將一直被阻塞直到可以獲得為止,如果發生死鎖,那麼Lock方法將無法終止。
  但是如果你呼叫帶超時的tryLock方法,那麼如果執行緒在等待時被中斷,將丟擲異常
  
  
讀寫鎖
  java.util.concurrent.locks包定義了兩個鎖類,我們已經討論過了reentrantlock和reentrantReadWriteLock類,當有很多執行緒都從某個資料結構中讀取資料而很少有執行緒對其
 進行修改時,後者就很有用了,在這種情況下,允許讀取器執行緒共享訪問是合適的,當然,寫入執行緒依然是必須是互斥訪問的,
 下面是使用讀寫鎖的必要步驟。
 1)建立一個reentrantreadwriteLock物件:
   private Reentrantreadwritelock rwl = new Reentrantreadwritelock();
 2) 抽取讀鎖和寫鎖
   private Lock rl = rwl.readLock();
   private LOck wl = rwl.writeLock();
 3) 對所有的訪問者加讀鎖
   public double getTotleBalance(){
    readLock.lock();
    try{}finally{readLock.unlock();}}
 4)對所有修改者加鎖
   public void transfer(...){
    writeLock.lock();
    try{}finally{writeLock.unlock();}}
    
    
為什麼要棄用stop和suspend方法
  1.0定義了stop和suspend方法,前者是用來直接終止執行緒,後者會阻塞執行緒直到另一個執行緒呼叫了resume。
 他們有一些共同點,都試圖專橫的控制一個給定執行緒的行為。
  首先我們來看看stop方法,這個方法將終止所有的未結束的方法,包括run方法,當一個執行緒停止時,他會立即釋放
 所有他鎖住的物件上的鎖,這會導致物件處於不一致的狀態,例如,假設一個執行緒在將錢從一個賬戶轉至另一個賬戶的過程中
 在取款之後存款之前停止了,那麼現在銀行物件就被破壞了,因為鎖已經釋放了,這種破損會被那些未被停止的執行緒所觀察到。
   當一個執行緒想終止另一個執行緒時,他無法知道何時呼叫stop方法才是安全的,何時會導致物件被破壞,因此這個方法被棄用了,
 你應該中斷一個執行緒而不是停止他,被中斷的執行緒在安全的時候停止。
 
   下面我們看看suspend方法會發生什麼問題,和stop不同,suspend不會破壞物件,但是,如果你用它掛起一個擁有鎖的執行緒
 那麼鎖在恢復之前不會被釋放,如果呼叫他的執行緒試圖取得相同的鎖,程式就會死鎖,被掛起的執行緒在等待恢復,而掛起他的執行緒在
 等待獲得鎖
 
 
阻塞佇列
  佇列是一種資料結構,他有兩個基本操作,在佇列尾部加入一個元素,和從佇列頭部移除一個元素。
 就是說,佇列以一種先進先出的方式管理資料。如果你試圖向一個已經滿了的阻塞佇列新增一個元素,或者從一個空的阻塞佇列中移除一個元素,將導致
 執行緒阻塞。在多執行緒進行合作時,阻塞佇列是和游泳的工具。工作者執行緒可以定期的把中間結果存在阻塞佇列轟炸那個,而其他的工作者執行緒吧中間結果取出
 並在將來修改他們,佇列會自動平衡負載,如果第一個執行緒集執行的比第二個慢,則第二個執行緒集在等待結果時會阻塞,如果第一個執行緒集執行的塊,那麼他將在
 第二個執行緒集趕上來。
   add           增加一個元素                     如果佇列已滿,則丟擲一個IllefalStateException異常
   remove        移除並返回佇列頭部的元素         如果佇列為空,則丟擲一個NoSuchElementException異常
   element       返回佇列頭部的元素               如果佇列為空,則丟擲一個NoSuchElementException異常
   offer         新增一個元素並返回true           如果佇列已滿,返回false
   poll          移除並返回佇列頭部的元素         如果佇列為空,則返回null
   peek          返回佇列頭部元素                 如果佇列為空,則返回null
   put           新增一個元素                     如果佇列已滿,則阻塞
   take          移除並返回佇列頭部的元素         如果佇列為空,則阻塞
 阻塞佇列的操作可以根據他們的響應方式分為三類,add,remove和element操作在你試圖為一個已滿的佇列中新增元素或者從一個空佇列中獲得元素時丟擲異常
 當然在多執行緒程式中,佇列在任何時間都可能變成滿的或則空的,所以你可以想使用,offer,poll,和peek方法,這些方法在無法完成任務時只是給出了一個出錯提示
 而不是丟擲異常。
 最後我們有阻塞操作put和take,put方法在佇列滿時阻塞,take在佇列空時阻塞,在不設定超時的情況下,offer和poll是等價的,
 
 java.util.concurrent包提供了阻塞佇列的4個變種,預設情況下,LinkedBlockingQueue的容量是沒有上限的,但是也可以選擇指定其的最大容量,ArrayBlockingQueue
 在構造時需要給定容量,並可以選擇是否需要公平性。如果公平性引數被設定了,等待最長時間的執行緒會優先得到處理,通常,公平性會使得你在效能上付出代價,只有在
 的確非常需要的時候再使用它
   PriorityBlockingQueue是一個帶優先順序的佇列,而不是先進先出佇列,元素按照優先順序排序被移除,該佇列也沒有上限,但是如果佇列是控,那麼取元素的操作就會被阻塞,
   最後,DelayQueue包含了實現了Delayed介面的物件:

執行緒安全的集合
  如果多執行緒要併發的修改一個數據結構,例如一個散列表,那麼就很容易破壞了這個資料結構。例如,一個執行緒可能要開始向表中插入一個新的元素了,假設他在改變散列表的元素之間的連線關係的過程中剝奪了控制權,那麼如果此時另一個執行緒也開始遍歷同一個列表,他
可能得到無效的節點,從而產生混亂,因此可能會丟擲異常或進入死迴圈。
  你可以通過提供鎖來保護共享資料結構,但是選擇一個執行緒安全的實現似乎更統一些,
  
  
  
高效佇列和散列表
  java.util.concurrent包提供了佇列和散列表的高效實現:ConcurrentLinkedQueue和ConcurrentHashMap。併發散列表能夠有效的支援多個讀取器操作和固定數量的寫入器操作。預設情況下,他假設可以有多達16個寫入器執行緒同時進行,讀取器可能更多。
  但如果同時存在寫入器執行緒超過了16個,其中一些就可能被暫時阻塞,你可以在構造器制定更大的容許寫入器執行緒數目,但這似乎不會用到。
  這些集合返回弱一致性的迭代器。這意味著迭代器不一定能夠反映出他們被構造之後所做的所有的修改,但他們不會兩次返回同一個值,也不會丟擲異常
  
  
  

 寫陣列的拷貝
  CopyOnWriteArrayList和CopyOnWriteArraySet是執行緒安全的集合,其中所有對他們的修改執行緒都會擁有一個底層陣列拷貝
 當集合上的跌代執行緒數目大大多於修改執行緒時,這種安排就顯得十分有用了,當你構建的是一個迭代器時,他包含對當前陣列的引用,如果陣列在後來被修改了,那麼迭代器依然
 引用舊的陣列,但此時集合的陣列已經被替換了,因此,較舊的迭代器用有一致的檢視,訪問他無需任何同步開銷。
 
 
 舊的執行緒安全的集合
 Vector和HashTable類就分別提供了執行緒安全的動態陣列和散列表的實現,這些類被棄用了,ArrayList和HashMap類取代了他們,這些類不是執行緒安全的。他們使用了另一種機制。任何
 一種collection類都可以通過同步包裝器變成執行緒安全的:
   List synvhArrayList = Collections.synchronizedList(new ArrayList());
   Map syncHashMap = Collections.synchronizedMap(new HashMap());
 
 經過同步包裝的collection中的方法由一個鎖保護起來,從而提供了執行緒安全的訪問,但是如果你想迭代這個collection,你就必須使用一個同步塊:
  synchronized(syncHashMap){
   Iterator iter = synchHashMap.keySet.iterator();
   while(iter.hashNext())
   }
   
   
 Callable和Future
 Runnable封裝一個非同步執行的任務,你可以吧他想想成一個沒有任何引數和返回值的非同步方法,Callable和runnable相似,但是他有返回值,CallBale介面是一個引數化的型別,只有一個
 方法call
 
  public interface Callable<V>{
   V call() throws Exception;}
   
 型別引數是返回值的型別,例如,Callable<Integer>代表一個最終返回Integer物件的非同步計算.
 Future儲存非同步計算的結果,當你使用future物件時,你就可以啟動一個計算,把計算結果給某個執行緒,
 然後就去幹你自己的事,future物件的所有者在結果計算好之後就可以得到他,
 future介面具有下面的方法:
  public interface Future<V>{
   V get() throws ...
   V get(long timeout ,TimeUnit unit) throws ...
   void cancel(boolean mayinterrupt);
   boolean isCanelled();
   boolean isDone();
  }
 第一個get方法的呼叫將被阻塞,直到計算完成,第二個get方法的呼叫如果在計算完成之前超時,那麼將會丟擲異常,如果執行計算的執行緒被中斷
這兩個方法都會丟擲異常,如果計算已經完成,那麼get方法將立即返回。
  如果計算還在進行中,isDone方法將返回false,如果計算完成就返回ture。
  你可以使用cancel方法來取消計算,如果計算還沒喲開始,他會被取消永遠不會開始。如果計算正在進行,那麼,如果Mayinterrupt引數值為ture,
 他就會被中斷。
  FutureTask包裝器是一種很方便的將Callable轉換成Future和Runnable的機制,他同時實現了兩者的介面。
  
  
執行器
  構建一個新的執行緒的代價還是有些高的,因為他涉及與作業系統的互動,如果你的程式建立 大量生存期很短的執行緒,那就應該使用執行緒池,一個執行緒池包含大量準備
  執行的空閒執行緒,你將一個runnable物件給執行緒池,執行緒池中的一個執行緒就會呼叫run方法。當run方法退出時,執行緒不會死亡,而是繼續線上程池中準備為下一個請求提供服務。
  另一個使用執行緒池的理由是減少併發執行緒的數量,建立大量的執行緒會降低效能甚至導致虛擬機器崩潰,如果你用的演算法會建立許多執行緒,那麼就應該使用一個執行緒數固定的執行緒池來限制
  併發執行緒的數量。
  
  
 執行緒池
   newCachedThreadPoll方法構建了一個執行緒池,對於每個人物,如果有空閒執行緒可用,立即讓他執行任務,否則建立一個新的執行緒。newFixedThreadPool方法建立一個大小固定的執行緒池。
  如果提交的任務數大於空閒執行緒數,那麼得不到服務的任務將被置於佇列中,當其他任務完成後他們就能運行了。newSingleThreadExecutor是一個退化了大小為1的執行緒池,由一個執行緒執行所有任務
  一個接一個,這三個方法返回一個實現了executorService介面的ThreadPoolExecutor類的物件。
  
  當用完一個連線池後,要呼叫shutdown,這個方法將啟動池的關閉序列,被關閉的執行器不再接受新的任務,當所有任務都完成以後,池中的執行緒就死亡了,另外,還可以呼叫shutdownNow,池會取消所有還沒有開始的任務
  並試圖中斷正在執行的執行緒。
  
  下面總結了在使用執行緒池時應該做的事:
  1)呼叫Executors類中靜態的newCachedThreadPool或newFixedThreadPool方法。
  2)呼叫submit來提交一個runnable或callable物件。
  3)如果希望能夠取消任務或如果提交了一個callable物件,那就儲存好返回的future物件。
  4)當不想在提交任何任務時呼叫shutdown。
  
  

集合

集合介面
  將集合介面和實現分離
 與現代資料結構類庫中的常見情況一樣,java集合類庫也將介面和實現分離了,讓我們來看一看我們熟悉的一種資料結構---“佇列"中的這種分離情況
 佇列介面規定你可以在佇列尾部新增元素,刪除佇列頭部的元素,並且可以查詢佇列中有多少個元素。當你需要收集物件並且按照先進先出原則來檢索物件
 時,你就可以使用佇列
   一個佇列介面的最小形式可能類似下面的形式: