血洗多執行緒,抱得 offer 歸

lizhiqiang666發表於2019-06-14

工作和麵試之中,遇到了很多多執行緒問題。這裡我總結了一下,希望對你有所幫助。本篇內容,基本上都是一些反例,有些很低階但常見。
當然,面試時拿來裝逼用,也是極好的。
先來10個。
我來評個級
一、玩命的建立執行緒池
現象: 系統資源耗盡,程式僵死。
原因: 每次方法執行,都new一個執行緒池。

解決方式:共用一個執行緒池即可。

作死等級: 五顆星
腦殘等級: 五顆星

void doJob(){
    ThreadPoolExecutor exe = new ThreadPoolExecutor(...);
    exe.submit(new Runnable(){...})
}

複製程式碼二、鎖洩漏
現象: 某個執行緒一直持有鎖而不釋放,造成鎖洩漏。
原因: 未知異常或邏輯導致unlock函式未執行。

解決方式:始終將unlock函式放在finally中。

作死等級: 三顆星
腦殘等級: 四顆星

private final Lock lock = new ReentrantLock();
void doJob(){
    try{
        lock.lock();
        //do. sth
        lock.unlock();
    }catch(Exception e){
    }
}

複製程式碼三、忘記同步變數
現象: 在某個條件下,丟擲IllegalMonitorStateException。
原因: 呼叫wait、notify等,忘記synchronized,或者同步了錯誤的變數。

解決方式:呼叫這些函式之前,要使用同步關鍵字同步它。

作死等級: 兩顆星
腦殘等級: 四顆星
Object condition = new Object();

condition.wait();
複製程式碼四、HashMap死迴圈
現象: cpu佔用高,發生死迴圈,使用jstack檢視是阻塞在get方法上。
原因: 在某種條件下,進行rehash時,會形成環形鏈。某些get請求會走到這個環上。

解決方式:多執行緒環境下,使用ConcurrentHashMap,別猶豫。

作死等級: 四顆星
腦殘等級: 四顆星
五、給同步的變數重新賦值
現象: 不能夠達到同步效果,結果是錯誤的。
原因: 非基本型別被重新賦值,會改變鎖的指向,不同執行緒持有的鎖可能不一樣。

解決方式:把鎖物件宣告為final型別。

作死等級: 四顆星
腦殘等級: 三顆星

List listeners = new ArrayList();

void add(Listener listener, boolean upsert){
    synchronized(listeners){
        List results = new ArrayList();
        for(Listener ler:listeners){
        ...
        }
        listeners = results;
    }
}

複製程式碼六、執行緒迴圈未捕獲異常
現象: 執行緒作業無法繼續執行,不明終止。
原因: 未捕獲迴圈中的異常,造成執行緒退出。

解決方式:習慣性捕獲所有異常。

作死等級: 三顆星
腦殘等級: 三顆星

volatile boolean run = true;
void loop(){
    while(run){
        //do . sth
        int a = 1/0;
    }
}

複製程式碼七、volatile誤作計數器
現象: 多執行緒計數結果有誤。
原因: volatile保證可見性,不保證原子性,多執行緒操作並不能保證其正確性。

解決方式:直接使用Atomic類。

作死等級: 三顆星
腦殘等級: 兩顆星

volatile count = 0;
void add(){
    ++count;
}

複製程式碼八、錯誤保護範圍
現象: 雖然使用了執行緒安全的集合,但達不到同步效果。
原因: 操作要修改多個執行緒安全的集合,但操作本身不是原子的。

解決方式:弄明白要保護的程式碼邏輯域。

作死等級: 三顆星
腦殘等級: 四顆星

private final ConcurrentHashMap<String,Integer> nameToNumber;
private final ConcurrentHashMap<Integer,Salary> numberToSalary;
public int geBonusFor(String name) {
    Integer serialNum = nameToNumber.get(name);
    Salary salary = numberToSalary.get(serialNum);
    return salary.getBonus();
}

複製程式碼再比如下面的錯誤程式碼。

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

if(!map.containsKey("foo"))
    map.put("foo", "bar");

複製程式碼九、一些老的日期處理類
現象: 使用全域性的Calendar,SimpleDateFormat等進行日期處理,發生異常或者資料不準確。
原因: 這倆東西不是執行緒安全的,併發呼叫會有問題。

解決方式:放在ThreadLocal中,建議使用執行緒安全的DateTimeFormatter。

作死等級: 三顆星
腦殘等級: 三顆星

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

Date dododo(String str){
    return format(str);
}

複製程式碼十、程式碼死鎖
現象: 程式碼產生死鎖和相互等待。
原因: 程式碼滿足了下面四個條件:互斥;不可剝奪;請求和保持;迴圈等待。

解決方式:破壞這四個條件。或者少用同步。

作死等級: 兩顆星
腦殘等級: 一顆星
下面是一段簡單的死鎖程式碼。

final Object lock1 = new Object();
final Object lock2 = new Object();
new Thread(new Runnable() {
    @Override
    public void run() {
        sleep(1000);
        synchronized (lock1) {
            synchronized (lock2) {
            }
        }
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (lock2) {
            sleep(1000);
            synchronized (lock1) {
            }
        }
    }
}).start();

複製程式碼十一、long變數讀取無效值
現象: 會讀取到非設定的值。
原因: long變數讀寫不是原子的,可能會讀到1個變數的高32位和另一個變數的低32位位元組。

解決方式:確保long和double變數的資料正確,可以加上volatile關鍵字。

作死等級: 一顆星
腦殘等級: 沒有星

擴充套件閱讀(jdk10):docs.oracle.com/javase/spec…

咦?怎麼有11個?一定是多執行緒計算錯誤。

End
許多java開發,都是剛剛接觸多執行緒開發。但即使是有經驗的開發,也會陷入很多多執行緒的陷阱。當你的程式沒有得相應的期望,希望本文能幫你瞭解到其中的微妙之處。
多執行緒的使用是及其複雜的,使用低階api出錯的概率會成倍增加,對技能要求也較高。所幸,concurrent包使得這個過程方便了很多,但依然存在資源規劃和同步失效的問題。小姐姐味道這裡一個比較淺顯但全面的總結:JAVA多執行緒使用場景和注意事項簡版,但健壯的程式碼還要靠你自己去實踐呀。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章