java多線程的安全性

線程安全性

定義:當多個線程訪問某個類時,不管運行時環境採用何種調度方式,或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

1. 原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行訪問。

Atomic包:

  1. AtomicXXX:CAS、Unsafe.compareAndSwapInt
  2. AtomicLong、LongAdder
  3. AtomicReference、AtomicReferenceFieldUpdater
  4. AtomicStampReference:CAS的ABA問題

原子性 - synchronized(同步鎖)
修飾代碼塊:大括號括起來的代碼,作用於調用的對象
修飾方法:整個方法,作用於調用的對象
修飾靜態方法:整個靜態方法,作用於所有對象
修飾類:括號括起來的部分,作用於所有類
原子性 - 對比
synchronized:不可中斷鎖,適合競爭不激烈,可讀性好
Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態
Atomic:競爭激烈時能維持常態,比Lock性能好;只能同步一個值

2. 可見性:一個線程對主內存的修改可以及時的被其他線程觀察到。

導致共享變量在線程見不可見的原因

  1. 線程交叉執行
  2. 衝排序結合線程交叉執行
  3. 共享變量更新後的值沒有在工作內存與主內存之間急事更新

synchronized、volatile
JMM關於synchronized的兩條規定:

  1. 線程解鎖前,必須把共享變量的最新制刷新到主內存
  2. 線程加鎖前,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖

volatile - 通過加入內存屏障禁止重排序優化來實現

  1. 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存
  2. 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
  3. volatile變量在每次被線程訪問時,都強迫從主內存中讀取該變量的值,而當變量的值發生變化時,又會強迫線程將該變量最新的值強制刷新到主內存,這樣一來,任何時候不同的線程總能看到該變量的最新值

3. 有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序。

Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。volatile、synchronized、Lock。
【volatile變量規則】:對一個變量的寫操作先行發生於後面對這個變量的讀操作。(如果一個線程進行寫操作,一個線程進行讀操作,那麼寫操作會先行於讀操作。)
【傳遞規則】:如果操作A先行於操作B,而操作B又先行於操作C,那麼操作A就先行於操作C。
【線程啓動規則】:Thread對象的start方法先行發生於此線程的每一個動作。
【線程中斷規則】:對線程interrupt方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
【線程終結規則】:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()方法的返回值手段檢測到線程已經終止執行。
【對象終結規則】:一個對象的初始化完成先行發生於他的finalize()方法的開始。

發佈對象

發佈對象:使一個對象能夠被當前範圍之外的代碼所用。
對象溢出:一種錯誤的發佈。當一個對象還沒有構造完成時,就使它被其他線程所見。

安全發佈對象

在靜態初始化函數中初始化一個對象
將對象的引用保存到volatile類型域或者AtomicReference對象中
將對象的引用保存到某個正確構造對象的final類型域中
將對象的引用保存到一個由鎖保護的域中

/**
 * 懶漢模式
 * 雙重同步鎖單例模式
 * @author Guo
 *
 */
public class SingletonExample1 {
    
    private SingletonExample1(){
        
    }
    
    // volatile禁止指令重排
    private volatile static SingletonExample1 instance = null;
    
    public static SingletonExample1 getInstance(){
        if(instance == null){
            synchronized(SingletonExample1.class){
                if(instance == null){
                    instance = new SingletonExample1();
                }
            }
        }
        return instance;
    }

}

避免併發兩種方式

  1. 不可變對象
  2. 線程封閉

線程封閉: 把對象封裝到一個線程裏,只有這一個線程可以看到這個對象,即使這個對象不是線程安全也不會出現任何線程安全問題,因爲只在一個線程裏

  1. 堆棧封閉局部變量,無併發問題。棧封閉是我們編程當中遇到的最多的線程封閉。什麼是棧封閉呢?簡單的說就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一分兒到線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現併發問題。所以能用局部變量就別用全局的變量,全局變量容易引起併發問題。
  2. ThreadLocal線程封閉:比較推薦的線程封閉方式。
    【ThreadLocal結合filter完成數據保存到ThreadLocal裏,線程隔離。】通過filter獲取到數據,放入ThreadLocal, 當前線程處理完之後interceptor將當前線程中的信息移除。使用ThreadLocal是實現線程封閉的最好方法。ThreadLocal內部維護了一個Map,Map的key是每個線程的名稱,而Map的值就是我們要封閉的對象。每個線程中的對象都對應着Map中一個值,也就是ThreadLocal利用Map實現了對象的線程封閉

線程不安全類與寫法

【線程不安全】:如果一個類類對象同時可以被多個線程訪問,如果沒有做同步或者特殊處理就會出現異常或者邏輯處理錯誤。
【1. 字符串拼接】:
StringBuilder(線程不安全)、
StringBuffer(線程安全)
【2. 日期轉換】: 
SimpleDateFormat(線程不安全,最好使用局部變量[堆棧封閉]保證線程安全)
JodaTime推薦使用(線程安全)
【3. ArrayList、HashSet、HashMap等Collections】: 
ArrayList(線程不安全)
HashSet(線程不安全)
HashMap(線程不安全)
【**同步容器**synchronized修飾】
Vector、Stack、HashTable
Collections.synchronizedXXX(List、Set、Map)
【**併發容器** J.U.C】
ArrayList -> CopyOnWriteArrayList:(讀時不加鎖,寫時加鎖,避免複製多個副本出來將數據搞亂)寫操作時複製,當有新元素添加到CopyOnWriteArrayList中時,先從原有的數組中拷貝一份出來,在新的數組上進行寫操作,寫完之後再將原來的數組指向新的數組。

HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet
HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap
相比ConcurrentHashMap,ConcurrentSkipListMap具有如下優勢:

  1. ConcurrentSkipListMap的存取速度是ConcurrentSkipListMap的4倍左右
  2. ConcurrentSkipListMap的key是有序的
  3. ConcurrentSkipListMap支持更高的併發(它的存取時間和線程數幾乎沒有關係,更高併發的場景下越能體現出優勢)

安全共享對象策略 - 總結

  1. 線程限制:一個被線程限制的對象,由線程獨佔,並且只能被佔有它的線程修改
  2. 共享只讀:一個共享只讀的對象,在沒有額外同步的情況下,可以被多個線程併發訪問,但是任何線程都不能修改它
  3. 線程安全對象:一個線程安全的對象或者容器,在內部通過同步機制來保證線程安全,所以其他線程無需額外的同步就可以通過公共接口隨意訪問它
  4. 被守護對象:被守護對象只能通過獲取特定鎖來訪問

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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