讀阿里Java開發手冊後的一些整理

之前阿里巴巴公衆號推送了一條消息,內容是阿里內部整理的Java開發手冊文檔,全篇讀完以後,整理出一份覺得對可能對大家有所幫助的信息。

1.POJO 類中布爾類型的變量,都不要加 is,否則部分框架解析會引起序列化錯誤。

反例:定義爲基本數據類型boolean isSuccess;的屬性,它的方法也是isSuccess(),RPC 框架在反向解析的時候,“以爲”對應的屬性名稱是 success,導致屬性獲取不到,進而拋出異常。

2.包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用 單數形式,但是類名如果有複數含義,類名可以使用複數形式。

正例: 應用工具類包名爲com.alibaba.open.util、類名爲MessageUtils(此規則參考 spring 的框架結構)

3.不要使用一個常量類維護所有常量,應該按常量功能進行歸類,分開維護。如:緩存 相關的常量放在類:CacheConsts 下;系統配置相關的常量放在類:ConfigConsts 下。

說明:大而全的常量類,非得使用查找功能才能定位到修改的常量,不利於理解和維護

4.所有的相同類型的包裝類對象之間值的比較,全部使用 equals 方法比較。

說明:對於Integer var=?在-128至127之間的賦值,Integer對象是在 IntegerCache.cache 產生,會複用已有對象,這個區間內的 Integer 值可以直接使用==進行 判斷,但是這個區間之外的所有數據,都會在堆上產生,並不會複用已有對象,這是一個大坑, 推薦使用 equals 方法進行判斷。

5.所有的POJO類屬性必須使用包裝數據類型,RPC方法的返回值和參數必須使用包裝數據類型,所有的局部變量【推薦】使用基本數據類型

6.關於 hashCode 和 equals 的處理,遵循如下規則

  • 只要重寫equals,就必須重寫hashCode。
  • 因爲Set存儲的是不重複的對象,依據hashCode和equals進行判斷,所以Set存儲的 對象必須重寫這兩個方法。
  • 如果自定義對象做爲Map的鍵,那麼必須重寫hashCode和equals
  • String 重寫了 hashCode 和 equals 方法,所以我們可以非常愉快地使用 String 對象 作爲 key 來使用。

7.不要在 foreach 循環裏進行元素的 remove/add 操作。remove 元素請使用 Iterator方式,如果併發操作,需要對 Iterator 對象加鎖。

8.使用 entrySet 遍歷 Map 類集合 KV,而不是 keySet 方式進行遍歷

說明:keySet 其實是遍歷了 2 次,一次是轉爲 Iterator 對象,另一次是從 hashMap 中取出 key 所對應的 value。而 entrySet 只是遍歷了一次就把 key 和 value 都放到了 entry 中,效 率更高。如果是 JDK8,使用 Map.foreach 方法

9.高度注意 Map 類集合 K/V 能不能存儲 null 值的情況

集合類 Key Value Super 說明
Hashtable 不允許爲 null 不允許爲 null Dictionary 線程安全
ConcurrentHashMap 不允許爲 null 不允許爲 null AbstractMap 分段鎖技術
TreeMap 不允許爲 null 允許爲 null AbstractMap 線程不安全
HashMap 允許爲 null 允許爲 null AbstractMap 線程不安全

反例: 由於 HashMap 的干擾,很多人認爲 ConcurrentHashMap 是可以置入 null 值,注意存儲 null 值時會拋出 NPE 異常

10.獲取單例對象需要保證線程安全,其中的方法也要保證線程安全

資源驅動類、工具類、單例工廠類都需要注意。

11.線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣 的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

Executors 返回的線程池對象的弊端如下

  • FixedThreadPool 和 SingleThreadPool:允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool:允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

12.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 代替 Simpledateformatter,官方給出的解釋:simple beautiful strong immutable thread-safe。

13.高併發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能 鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。

14.對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造 成死鎖。

線程一需要對錶 A、B、C 依次全部加鎖後纔可以進行更新操作,那麼線程二的加鎖順序 也必須是 A、B、C,否則可能出現死鎖。

15.併發修改同一記錄時,避免更新丟失,要麼在應用層加鎖,要麼在緩存加鎖,要麼在 數據庫層使用樂觀鎖,使用 version 作爲更新依據

如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次 數不得小於 3 次。

16.多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲 拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題

17.使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown方法,線程執行代碼注意 catch 異常,確保 countDown 方法可以執行,避免主線程無法執行 至 countDown 方法,直到超時才返回結果。

說明:注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。

18.避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一 seed 導致的性能下降。

在 JDK7 之後,可以直接使用 API ThreadLocalRandom,在 JDK7 之前,可以做到每個 線程一個實例。

19.通過雙重檢查鎖(double-checked locking)(在併發場景)實現延遲初始化的優 化問題隱患(可參考 The “Double-Checked Locking is Broken” Declaration),推薦問題 解決方案中較爲簡單一種(適用於 JDK5 及以上版本),將目標屬性聲明爲 volatile 型

反例:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null)
         synchronized(this) {
           if (helper == null)
                helper = new Helper();
         }
            return helper;
    }
        // other functions and members...
}

20.volatile 解決多線程內存不可見問題。對於一寫多讀,是可以解決變量同步問題, 但是如果多寫,同樣無法解決線程安全問題。如果是 count++操作,使用如下類實現

AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推 薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。

21.HashMap 在容量不夠進行 resize 時由於高併發可能出現死鏈,導致 CPU 飆升,在 開發過程中注意規避此風險。

22.ThreadLocal 無法解決共享對象的更新問題,ThreadLocal 對象建議使用 static 修飾。這個變量是針對一個線程內所有操作共有的,所以設置爲靜態變量,所有此類實例共享 此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只 要是這個線程內定義的)都可以操控這個變量。

23.不能在 finally 塊中使用 return,finally 塊中的 return 返回後方法結束執行,不 會再執行 try 塊中的 return 語句

24.應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架SLF4J 中的 API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

25.對 trace/debug/info 級別的日誌輸出,必須使用條件輸出形式或者使用佔位符的方式。

說明:logger.debug(“Processing trade with id: ” + id + ” symbol: ” + symbol); 如果日誌級別是 warn,上述日誌不會打印,但是會執行字符串拼接操作,如果 symbol 是對象, 會執行 toString()方法,浪費了系統資源,執行了上述操作,最終日誌卻沒有打印。

正例:(條件)

if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}

正例:(佔位符)

logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);

26.避免重複打印日誌,浪費磁盤空間,務必在 log4j.xml 中設置 additivity=false。

正例:

<logger name="com.taobao.dubbo.config" additivity="false">

27.異常信息應該包括兩類信息:案發現場信息和異常堆棧信息。如果不處理,那麼往上拋。

正例:

logger.error(各類參數或者對象toString + "_" + e.getMessage(), e);

28.建表時小數類型爲 decimal,禁止使用 float 和 double。

float 和 double 在存儲的時候,存在精度損失的問題,很可能在值的比較時,得到不 正確的結果。如果存儲的數據範圍超過 decimal 的範圍,建議將數據拆成整數和小數分開存儲。

29.varchar 是可變長字符串,不預先分配存儲空間,長度不要超過 5000,如果存儲長 度大於此值,定義字段類型爲 text,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率。

30.如果有 order by 的場景,請注意利用索引的有序性。order by 最後的字段是組合 索引的一部分,並且放在索引組合順序的最後,避免出現 file_sort 的情況,影響查詢性能。

正例:where a=? and b=? order by c; 索引:a_b_c

反例:索引中有範圍查找,那麼索引有序性無法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 無法排序。

31.利用延遲關聯或者子查詢優化超多分頁場景。

說明:MySQL 並不是跳過 offset 行,而是取 offset+N 行,然後返回放棄前 offset 行,返回 N 行,那當 offset 特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過 特定閾值的頁數進行 SQL 改寫。

正例:先快速定位需要獲取的 id 段,然後再關聯:

SELECT a.* FROM 表 1 a, (select id from 表 1 where 條件 LIMIT 100000,20 ) b where a.id=b.id

32.SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好。

  • 1.consts 單表中最多隻有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到數據。
  • 2.ref 指的是使用普通的索引(normal index)。
  • 3.range 對索引進行範圍檢索。

反例:explain 表的結果,type=index,索引物理文件全掃描,速度非常慢,這個 index 級 別比較 range 還低,與全表掃描是小巫見大巫

33.建組合索引的時候,區分度最高的在最左邊

如果 where a=? and b=? ,a 列的幾乎接近於唯一值,那麼只需要單建 idx_a 索引即 可。

存在非等號和等號混合判斷條件時,在建索引時,請把等號條件的列前置。如:where a>? and b=? 那麼即使 a 的區分度更高,也必須把 b 放在索引的最前列。

34.不要使用 count(列名)或 count(常量)來替代 count(*),count(*)就是 SQL92 定義 的標準統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。

count(*)會統計值爲 NULL 的行,而 count(列名)不會統計此列爲 NULL 值的行。

35.count(distinct col) 計算該列除 NULL 之外的不重複數量。注意 count(distinct col1, col2) 如果其中一列全爲NULL,那麼即使另一列有不同的值,也返回爲0。

36.當某一列的值全是 NULL 時,count(col)的返回結果爲 0,但 sum(col)的返回結果爲 NULL,因此使用 sum()時需注意 NPE 問題。

正例:可以使用如下方式來避免sum的NPE問題:SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;

37.使用 ISNULL()來判斷是否爲 NULL 值。注意:NULL 與任何值的直接比較都爲 NULL

38.不得使用外鍵與級聯,一切外鍵概念必須在應用層解決。

學生表中的 student_id 是主鍵,那麼成績表中的 student_id 則爲外鍵。 如果更新學生表中的 student_id,同時觸發成績表中的 student_id 更新,則爲級聯更新。 外鍵與級聯更新適用於單機低併發,不適合分佈式、高併發集羣;級聯更新是強阻塞,存在數據庫更新風暴的風險;外鍵影響數據庫的插入速度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章