《Effective Java》——學習筆記(異常&並發)__Java

來源:互聯網
上載者:User
異常 第57條:只針對異常的情況才使用異常

異常應該只用於異常的情況下:它們永遠不應該用於正常的控制流程

設計良好的API不應該強迫它的用戶端為了正常的控制流程而使用異常 第58條:對可恢複的情況使用受檢異常,對編程錯誤使用運行時異常

Java程式設計語言提供了三種可拋出結構:受檢的異常、運行時異常和錯誤

如果期望調用者能夠適當地恢複,對於這種情況就應該使用受檢的異常,通過拋出受檢的異常,強迫調用者在一個catch子句中處理該異常,或者將它傳播出去。因此,方法中聲明要拋出的每個受檢的異常,都是對API使用者的一種潛在指示:與異常相關聯的條件是調用這個方法的一種可能的結果

如果程式拋出未受檢的異常或者錯誤,往往就屬於不可恢複的情形,繼續執行下去有害無益

錯誤往往被JVM保留用於表示資源不足、約束失敗或者其他使程式無法繼續執行的條件,由於這已經是個幾乎被普遍接受的慣例,因此最好不要再實現任何新的Error子類,因此,所有未受檢的拋出結構都應該是RuntimeException的子類(直接的或者間接的) 第59條:避免不必要地使用受檢的異常

過分使用受檢的異常會使API使用起來非常不便,如果正確地使用API並不能阻止這種異常條件的產生,並且一旦產生異常,使用API的程式員可以立即採取有用的動作,這時受檢異常才應該被使用 第60條:優先使用標準的異常

Java平台類庫提供了一組基本的未受檢的異常,它們滿足了絕大多數API的異常拋出需要,重用現有的異常有很多方面的好處,如更加易於學習和使用,可讀性會更好等

常用的異常: illegalArgumentException 當調用者傳遞的參數值不合適的時候,往往就會拋出這個異常 illegalStateException 如果因為接受對象的狀態而使調用非法,通常就會拋出這個異常 NullPointerException 參數中傳遞了null IndexOutOfBoundsException 下標越界 ConcurrentModificationException 如果一個對象被設計為專用於單線程或者與外部同步機制配合使用,一旦發現它正在(或已經)被並發地修改,就應該拋出這個異常 UnsupportedOperationException 如果對象不支援所請求的操作,就會拋出這個異常 第61條:拋出與抽象相對應的異常

更高層的實現應該捕獲低層的異常,同時拋出可以按照高層抽象進行解釋的異常,這種做法被稱為異常轉譯

public E get(int index){    ListIterator<E> i = listIterator(index);    try {        return i.next();    } catch(NoSuchElementException e){        throw new IndexOutOfBoundsException("Index: " + index);    }}

一種特殊的異常轉譯形式稱為異常鏈,如果底層的異常對於調試導致高層異常的問題非常有協助,使用異常鏈就很合適

try{    ...} catch(LowerLevelException cause){    throw new HigherLevelException(cause);}class HigherLevelException extends Exception{    HigherLevelException(Throwable cause){        super(cause);    }}

如果不能阻止或者處理來自更低層的異常,一般的做法是使用異常轉譯,除非低層方法碰巧可以保證它拋出的所有異常對高層也合適才可以將異常從低層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:它允許拋出適當的高層異常,同時又能捕獲底層的原因進行失敗分析 第62條:每個方法拋出的異常都要有文檔

始終要單獨地聲明受檢的異常,並且利用Javadoc的@throws標記,準確地記錄下拋出每個異常的條件,但是不要使用throws關鍵字將未受檢的異常包含在方法的聲明中 第63條:在細節訊息中包含能捕獲失敗的資訊

為了捕獲失敗,異常的細節資訊應該包含所有“對該異常有貢獻”的參數和域的值。例如,IndexOutOfBoundsException異常的細節資訊應該包含下界、上界以及沒有落在界內的下標值

為了確保在異常的細節訊息中包含足夠的能捕獲失敗的資訊,一種辦法是在異常的構造器而不是字串細節訊息中引入這些資訊,然後,有了這些資訊,只要把它們放到訊息描述中,就可以自動產生細節訊息。例如,IndexOutOfBoundsException可以有個這樣的構造器:

public IndexOutOfBoundsException(int lowerBound, int upperBound, int index){    super("Lower bound: " + lowerBound +           ", Upper bound: " + upperBound +           ", Index: " + index);    // Save failure information for programmatic access    this.lowerBound = lowerBound;    this.upperBound = upperBound;    this.index = index;}
第64條:努力使失敗保持原子性

失敗的方法調用應該使對象保持在被調用之前的狀態,具有這種屬性的方法被稱為具有失敗原子性

對於可變對象,可以通過在執行操作之前檢查參數的有效性

public Object pop(){    if(size == 0)        throw new EmptyStackException();    Object result = elements[--size];    elements[size] = null;    return result;}

一種類似的獲得失敗原子性的辦法是,調整計算處理過程的順序,使得任何可能會失敗的計算部分都在對象狀態被修改之前發生

另一種獲得原子性的辦法是,在對象的一份臨時拷貝上執行操作,當操作完成之後再用臨時拷貝中的結果代替對象的內容 第65條:不要忽略異常

當API的設計者聲明一個方法將拋出某個異常的時候,不應該忽略(空的catch塊)它

try{    ...}catch(SomeException e){}
並發 第66條:同步訪問共用的可變資料

為了線上程之間進行可靠的通訊,也為了互斥訪問,同步是必要的

如果對共用的可變資料的訪問不能同步,其後果將非常可怕,即使這個變數是原子可讀寫的(long和double類型不是原子的)基本類型。要阻止一個線程妨礙另一個線程,建議做法是讓第一個線程輪詢一個boolean域,這個域一開始為false,但是可以通過第二個線程設定為true,以表示第一個線程將終止自己。由於boolean域的讀和寫操作都是原子的,程式再訪問這個域的時候不再使用同步:

public class StopThread {    private static boolean stopRequested;    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(new Runnable() {            @Override            public void run() {                int i = 0;                while (!stopRequested) {                    i++;                }            }        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        stopRequested = true;    }}

由於上述代碼沒有同步,虛擬機器會將這個代碼:

while(!done)    i++;

轉變成這樣:

if(!done)    while(true)        i++;

這種JVM的最佳化會導致活性失敗:這個程式無法前進。修正這個問題的一種方式是同步訪問stopRequest域,如下:

public class StopThread {    private static boolean stopRequested;    private static synchronized void requestStop(){        stopRequested = true;    }    private static synchronized boolean stopRequested(){        return stopRequested;    }    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(new Runnable() {            @Override            public void run() {                int i = 0;                while (!stopRequested()) {                    i++;                }            }        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        requestStop();    }}

StopThread中被同步方法的動作即使沒有同步也是原子的,這些方法的同步只是為了它的通訊效果,而不是為了互斥訪問

也可以使用volatile修飾符不執行互斥訪問,但它可以保證任何一個線程在讀取該域的時候都將看到最近剛剛被寫入的值:

public class StopThread {    private static volatile boolean stopRequested;    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(new Runnable() {            @Override            public void run() {                int i = 0;                while (!stopRequested) {                    i++;                }            }        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        stopRequested = true;    }}

在使用volatile的時候務必要小心,考慮下面的方法,假設它要產生序號:

private static volatile int nextSerialNumber = 0;public static int generateSerialNumber(){    return nextSerialNumber++;}

因為增量操作符(++)不是原子的,它在nextSerialNumber域中執行兩項操作:首先它讀取值,然後寫回一個新值。如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取這個域,第二個線程就會與第一個線程一起看到同一個值,並返回相同的序號。這就是安全性失敗:這個程式會計算出錯誤的結果

修正如下:

private static final AtomicLong nextSerialNum = new AtomicLong();public static long generateSerialNumber(){    return nextSerialNum.getAndIncrement();}

讓一個線程在短時間內修改一個資料對象,然後與其他線程共用,這是可以接受的,只同步共用對象引用的動作,然後其他線程沒有進一步的同步也可以讀取對象,只有它沒有再被修改。這種對象被稱作事實上不可變,將這種對象引用從一個線程傳遞到其他的線程被稱作安全發布。安全發布對象引用有許多方法:可以將它儲存在靜態域中,作為類初始化的一部分;可以將它儲存在volatile域、final域或者通過正常鎖定訪問的域中;或者可以將它放到並發的集合中

簡而言之,當多個線程共用可變資料的時候,每個讀或者寫資料的線程都必須執行同步。如果只需要線程之間的互動通訊,而不需要互斥,volatile修飾符就是一種可以接受的同步形式,但要正確地使用它可能需要一些技巧 第67條:避免過度同步

過度同步可能會導致效能降低、死結,甚至不確定的行為。在一個被同步的地區內部,不要調用設計成要被覆蓋的方法,或者是由用戶端以函數對象的形式提供的方法,這樣的方法是外來的,不知道該方法會做什麼事情,也無法控制它,如下例:

public class ObservableSet<E> extends ForwardingSet<E> {    public ObservableSet(Set<E> set) { super(set); }    private final List<SetObserver<E>> observers =         new ArrayList<SetObserver<E>>();    public void addObserver(SetObserver<E> observer) {        synchronized(observers) {            observers.add(observer);        }    }    public boolean removeObserver(SetObserver<E> observer) {        synchronized(observers) {            return observers.remove(observer);        }    }    // This method is the culprit    private void notifyElementAdded(E element) {        synchronized(observers) {            for (SetObserver<E> observer : observers)                observer.added(this, element);        }    }    @Override public boolean add(E element) {        boolean added = super.add(element);        if (added)            notifyElementAdded(element);        return added;    }    @Override public boolean addAll(Collection<? extends E> c) {        boolean result = false;        for (E element : c)            result |= add(element);  // calls notifyElementAdded        return result;    }}

Observer通過調用addObserver方法預訂通知,通過調用removeObserver方法取消預訂

public static void main(String[] args) {    ObservableSet<Integer> set =        new ObservableSet<Integer>(new HashSet<Integer>());    set.addObserver(new SetObserver<Integer>() {        public void added(ObservableSet<Integer> s, Integer e) {            System.out.println(e);            if (e == 23) s.removeObserver(this);        }    });    for (int i = 0; i < 100; i++)        set.add(i);}

上述程式在列印出0~23的數字後,並沒有停止,而是拋出ConcurrentModificationException。問題在於,當notifyElementAdded調用觀察者的added方法時,它正處於遍曆observers列表的過程中。added方法調用可觀察集合的removeObserver方法,從而調用observers.remove,企圖在遍曆列表的過程中,將一個元素從列表中刪除,這是非法的

可以通過將外來方法的調用移除同步的代碼塊來解決這個問題

private void notifyElementAdded(E element) {    List<SetObserver<E>> snapshot = null;    synchronized(observers) {        snapshot = new ArrayList<>(observers);    }    for (SetObserver<E> observer : snapshot){        observer.added(this, element);    }}

通常,應該在同步地區內做儘可能少的工作,獲得鎖,檢查共用資料,根據需要轉換資料,然後放掉鎖。如果必須要執行某個耗時操作,則應該設法把這個操作移動同步地區的外面

永遠不要過度同步,在這個多核的時代,過度同步的實際成本並不是指擷取鎖所花費的CPU時間;而是指失去了並行的機會,以及因為需要確保每個核都有一個一致的記憶體視圖而導致的延遲,過度同步的另一項潛在開銷在於,它會限制VM最佳化代碼執行的能力

如果一個可變的類要並發使用,應該使這個類變成是安全執行緒的,通過內部同步,可以獲得明顯比從外部鎖定整個對象更高的並發性,否則,就不要在內部同步

如果在內部同步了類,就可以使用不同的方法來實現高並發性,例如分拆鎖、分離鎖和非阻塞並發控制

如果方法修改了靜態域,也必須同步對這個域的訪問,即使它往往只用於單個線程 第68條:executor和task優先於線程

java平台的java.util.concurrent.Executor是一個很靈活的基於介面的任務執行工具

如果編寫的是小程式,或者是輕載的伺服器,使用Executors.newCachedThreadPool通常是個不錯的選擇,因為它不需要配置,並且一般情況下能夠正確地完成工作。但是對於大負載的伺服器來說,緩衝的線程池就不是很好的選擇了,在緩衝的線程池中,被提交的任務沒有排成隊列,而是直接交給線程執行,如果沒有線程可用,就建立一個新的線程,如果伺服器負載得太重,以致它所有的CPU都安全被佔用了,當有更多的任務時,就會建立更多的線程,這樣只會使情況變得更糟。因此,在大負載的產品伺服器中,最好使用Executors.newFixedThreadPool,它提供了一個包含固定線程數目的線程池,或者為了最大限度地控制它,就直接使用ThreadPoolExecutor類 第69條:並發工具優先於wait和notify

正確地使用wait和notify比較困難,就應該使用更進階的並發工具來代替

並發集合為標準的集合介面(如List、Queue和Map)提供了高效能的並發實現,為了提供高並發性,這些實現在內部自己管理同步。這意味著用戶端無法原子地對並發集合進行方法調用,因此有些集合介面已經通過依賴狀態的修改操作進行了擴充,它將幾個基本操作合并到了單個原子操作中。例如,ConcurrentMap擴充了Map介面,並添加了幾個方法,包括putIfAbsent(key, value),當鍵沒有映射時會替它插入一個映射,並返回與鍵關聯的前一個值,如果沒有這樣的值,則返回null

ConcurrentHashMap除了提供卓越的並發性之外,速度也非常快,可以極大地提升並發應用程式的效能

有些集合介面已經通過阻塞操作進行了擴充,它們會一直等待(或阻塞)到可以成功執行為止。例如,BlockingQueue擴充了Queue介面,並添加了包括take在內的幾個方法,它從隊列中刪除並返回了頭元素,如果隊列為空白,就等待。這樣就允許將阻塞隊列用於工作隊列,也稱作生產者—消費者隊列,一個或者多個生產者線程在工作隊列中添加工作項目,並且當工作項目可用時,一個或者多個消費者線程則從工作隊列中取出隊列並處理工作項目,大多數的ExecutorService實現都使用BlockingQueue 第70條:執行緒安全性的文檔化

一個類為了可被多個安全執行緒地使用,必須在文檔中清楚地說明它所支援的執行緒安全性層級 不可變的——這個類的執行個體是不變的,所以,不需要外部的同步,這樣的例子包括String、Long和BigInteger 無條件的安全執行緒——這個類的執行個體是可變的,但是這個類有著足夠的內部同步,所以,它的執行個體可以被並發使用,無需任何外部同步,其例子包括Random和ConcurrentHashMap 有條件的安全執行緒——除了有些方法為進行安全的並發使用而需要外部同步之外,這種安全執行緒層級與無條件的安全執行緒相同,這樣的例子包括Collections.synchronized封裝返回的集合,它們的迭代器要求外部同步 非安全執行緒——這個類執行個體是可變的,為了並發地使用它們,客戶必須利用自己選擇的外部同步包圍每個方法調用(或者調用序列),這樣的例子包括通用的集合實現,例如ArrayList和HashMap 線程對立的——這個類不能安全地被多個線程並發使用,即使所有的方法調用都被外部同步包圍,線程對立的根源通常在於,沒有同步地修改待用資料,這種類是因為沒有考慮到並發性而產生的後果

每個類都應該說明或者安全執行緒註解,清楚地在文檔中說明它的安全執行緒屬性,synchronized修飾符與這個文檔毫無關係,有條件的安全執行緒類必須在文檔中指明“哪個方法調用序列需要外部同步,以及在執行這些序列的時候要獲得哪把鎖(通常情況下,指作用在執行個體自身上的那把鎖)”。如果編寫的是無條件的安全執行緒類,就應該考慮使用私人鎖對象來代替同步的方法,這樣可以防止用戶端程式和子類的不同步幹擾,能夠在後續的版本中靈活地對並發控制採用更加複雜的方法 第71條:慎用延遲初始化

延遲初始化是延遲到需要域的值時才將它初始化的這種行為。這種方法既適用於靜態域,也適用於執行個體域,雖然延遲初始化主要是一種最佳化,但它也可以用來打破類和執行個體初始化中的有害迴圈

延遲初始化降低了初始化類或者建立執行個體的開銷,卻增加了訪問被延遲初始化的域的開銷。如果域只在類的執行個體部分被訪問,並且初始化這個域的開銷很高,可能就值得進行延遲初始化

當有多個線程時,延遲初始化是需要技巧的,如果兩個或者多個線程共用一個延遲初始化的域,採用某種形式的同步是很重要的。在大多數情況下,正常的初始化要優先於延遲初始化

如果出於效能的考慮而需要對靜態域使用延遲初始化,就使用lazy initialization holder class模式

private static class FieldHolder{    static final FieldType field = computeFieldValue();}static FieldType getField(){    return FieldHolder.field;}

當getField方法第一次被調用時,它第一次讀取FieldHolder.field,導致FieldHolder類得到初始化

如果出於效能的考慮而需要對執行個體域使用延遲初始化,就使用雙重檢查模式,這種模式避免了在域被初始化之後訪問這個域時的鎖定開銷

private volatile FieldType field;FieldType getField(){    FieldType result = field;    if(result == null) { // First check (no locking)        synchronized(this){            result = field;            if(result == null){ // Second check (with locking)                field = result = computeFieldValue();            }        }    }    return result;}

如果需要延遲初始化一個可以接受重複初始化的執行個體域,可以使用單重檢查模式

private volatile FieldType field;private FieldType getField(){    FieldType result = field;    if(result == null){        field = result = computeFieldValue();    }    return result;}
第72條:不要依賴於線程調度器

要編寫健壯的、響應良好的、可移植的多線程應用程式,最好的辦法是確保可運行線程的平均數量不明顯多於處理器的數量。這使得線程調度器沒有更多的選擇:它只需要運行這些可啟動並執行線程,直到它們不再可運行為止。即使在根本不同的線程調度演算法下,這些程式的行為也不會有很大的變化 第73條:避免使用線程組

線程組並沒有提供太多有用的功能,而且它們提供的許多功能還都是有缺陷的,如果設計的一個類需要處理線程的邏輯組,或許可以使用線程池executor

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.