工作和麵試之中,遇到了很多多執行緒問題。這裡我總結了一下,希望對你有所幫助。本篇內容,基本上都是一些反例,有些很低階但常見。
當然,面試時拿來裝逼用,也是極好的。
先來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 協議》,轉載必須註明作者和本文連結