並發編程的常見約束
阿新 • • 發佈:2018-04-04
內存 sleep format .com 並發 sha 適用於 更新 所有 並發處理 :
1. 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
說明:資源驅動類、工具類、單例工廠類都需要註意。 2. 【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。 正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
3. 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明:使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。
說明:Executors 返回的線程池對象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。 6. 【強制】高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
說明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調用 RPC 方法。
7. 【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:線程一需要對表 A、B、C 依次全部加鎖後才可以進行更新操作,那麽線程二的加鎖順序也必須是 A、B、C,否則可能出現死鎖。三線程打印ABC的問題 8. 【強制】並發修改同一記錄時,避免更新丟失,需要加鎖。要麽在應用層加鎖,要麽在緩存加鎖,要麽在數據庫層使用樂觀鎖,使用 version 作為更新依據。
說明:如果每次訪問沖突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。
9. 【強制】多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
10. 【推薦】使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown方法,線程執行代碼註意 catch 異常,確保 countDown 方法被執行到,避免主線程無法執行至 await 方法,直到超時才返回結果。
說明:註意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
11. 【推薦】避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed 導致的性能下降。
說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線程持有一個實例。
反例:
class Singleton {
private Helper helper = null; public Helper getHelper() {
if (helper == null) synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other methods and fields...
} 13. 【參考】volatile 解決多線程內存不可見問題。 對於一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
14. 【參考】 HashMap 在容量不夠進行 resize 時由於高並發可能出現死鏈,導致 CPU 飆升,在開發過程中可以使用其它數據結構或加鎖來規避此風險。
15. 【參考】ThreadLocal 無法解決共享對象的更新問題,ThreadLocal 對象建議使用 static修飾。這個變量是針對一個線程內所有操作共享的,所以設置為靜態變量,所有此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只 要是這個線程內定義的)都可以操控這個變量。
如需了解更多,請看《阿裏巴巴Java開發手冊》;也可以加QQ群了解更多:603654340
1. 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
說明:資源驅動類、工具類、單例工廠類都需要註意。 2. 【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。 正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
3. 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明:使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。
ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(4. 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。new MyThread());
說明:Executors 返回的線程池對象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
//SingleThreadPoolnew ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
//CachedThreadPool
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>())
5. 【強制】SimpleDateFormat 是線程不安全的類,一般不要定義為 static 變量,如果定義為static,必須加鎖,或者使用 DateUtils 工具類。
正例:註意線程安全,使用 DateUtils。亦推薦如下處理:private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。 6. 【強制】高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
說明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調用 RPC 方法。
7. 【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:線程一需要對表 A、B、C 依次全部加鎖後才可以進行更新操作,那麽線程二的加鎖順序也必須是 A、B、C,否則可能出現死鎖。三線程打印ABC的問題 8. 【強制】並發修改同一記錄時,避免更新丟失,需要加鎖。要麽在應用層加鎖,要麽在緩存加鎖,要麽在數據庫層使用樂觀鎖,使用 version 作為更新依據。
說明:如果每次訪問沖突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。
9. 【強制】多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
10. 【推薦】使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown方法,線程執行代碼註意 catch 異常,確保 countDown 方法被執行到,避免主線程無法執行至 await 方法,直到超時才返回結果。
說明:註意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
11. 【推薦】避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed 導致的性能下降。
說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線程持有一個實例。
1 package com.mmz.OtherTest; 2 3 import java.util.Random; 4 import java.util.concurrent.ThreadLocalRandom; 5 6 /* 7 * 8 */ 9 public class test4 implements Runnable { 10 11 private String name; 12 //相當於是在池中創建的ThreadLocalRandom對象,只是第一次創建 13 //ThreadLocalRandom random = ThreadLocalRandom.current();//放到此處,和放到24行效果一樣,每次random都一樣, 14 15 //使用Random則每次都會產生新的對象,而使用jdk7版本中的ThreadLocalRandom.current()則只創建一次對象 16 //Random random = new Random();//放到此處,和放到28行效果一樣,每次random不一樣 17 18 private test4(String name) { 19 this.name = name; 20 } 21 22 @Override 23 public void run() { 24 // ThreadLocalRandom random = ThreadLocalRandom.current();//放到此處,和放到12行效果一樣,每次random都一樣 25 // System.out.println(random); 26 // int count = random.nextInt(10,50); 27 28 Random random = new Random();//使用Random則每次都會產生新的對象,而使用jdk7版本中的ThreadLocalRandom.current()則只創建一次對象 29 System.out.println(random); 30 // int count = random.nextInt(10); 31 32 //使用下面的方式速度比前兩種都慢,所以一般不使用;通過sleep(1000)都不能保證其執行順序,當調整睡眠時間更長時才能保證,可以得出該結論。 33 int count = (int) (Math.random()*10); 34 System.out.println(count);//生成一個10~50之間的隨機數 35 while (count > 0) { 36 System.out.print(name); 37 try { 38 Thread.sleep(1000); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 count--; 43 } 44 System.out.println(); 45 } 46 47 48 49 public static void main(String[] args) throws Exception { 50 Object a = new Object(); 51 Object b = new Object(); 52 Object c = new Object(); 53 test4 pa = new test4("A"); 54 test4 pb = new test4("B"); 55 test4 pc = new test4("C"); 56 57 new Thread(pa).start(); 58 Thread.sleep(1000); //確保按順序A、B、C執行 59 new Thread(pb).start(); 60 Thread.sleep(1000); 61 new Thread(pc).start(); 62 Thread.sleep(1000); 63 } 64 }test4 12. 【推薦】在並發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解決方案中較為簡單一種(適用於 JDK5 及以上版本),將目標屬性聲明為 volatile 型。
反例:
class Singleton {
private Helper helper = null; public Helper getHelper() {
if (helper == null) synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other methods and fields...
} 13. 【參考】volatile 解決多線程內存不可見問題。 對於一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
14. 【參考】 HashMap 在容量不夠進行 resize 時由於高並發可能出現死鏈,導致 CPU 飆升,在開發過程中可以使用其它數據結構或加鎖來規避此風險。
15. 【參考】ThreadLocal 無法解決共享對象的更新問題,ThreadLocal 對象建議使用 static修飾。這個變量是針對一個線程內所有操作共享的,所以設置為靜態變量,所有此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只 要是這個線程內定義的)都可以操控這個變量。
如需了解更多,請看《阿裏巴巴Java開發手冊》;也可以加QQ群了解更多:603654340
並發編程的常見約束