線程安全性
定義:當多個線程訪問某個類時,不管運行時環境採用何種調度方式
,或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同
,這個類都能表現出正確的行爲
,那麼就稱這個類是線程安全的。
1. 原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行訪問。
Atomic包:
- AtomicXXX:CAS、Unsafe.compareAndSwapInt
- AtomicLong、LongAdder
- AtomicReference、AtomicReferenceFieldUpdater
- AtomicStampReference:CAS的ABA問題
原子性 - synchronized(同步鎖)修飾代碼塊
:大括號括起來的代碼,作用於調用的對象修飾方法
:整個方法,作用於調用的對象修飾靜態方法
:整個靜態方法,作用於所有對象修飾類
:括號括起來的部分,作用於所有類
原子性 - 對比synchronized
:不可中斷鎖,適合競爭不激烈,可讀性好Lock
:可中斷鎖,多樣化同步,競爭激烈時能維持常態Atomic
:競爭激烈時能維持常態,比Lock性能好;只能同步一個值
2. 可見性:一個線程對主內存的修改可以及時的被其他線程觀察到。
導致共享變量在線程見不可見的原因:
- 線程交叉執行
- 衝排序結合線程交叉執行
- 共享變量更新後的值沒有在工作內存與主內存之間急事更新
synchronized、volatile
JMM關於synchronized的兩條規定:
- 線程解鎖前,必須把共享變量的最新制刷新到主內存
- 線程加鎖前,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(
注意:加鎖與解鎖是同一把鎖
)
volatile - 通過加入內存屏障
和禁止重排序
優化來實現
- 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存
- 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
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;
}
}
避免併發兩種方式
- 不可變對象
線程封閉
線程封閉: 把對象封裝到一個線程裏,只有這一個線程可以看到這個對象,即使這個對象不是線程安全也不會出現任何線程安全問題,因爲只在一個線程裏
堆棧封閉
:局部變量,無併發問題。
棧封閉是我們編程當中遇到的最多的線程封閉。什麼是棧封閉呢?簡單的說就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一分兒到線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現併發問題。所以能用局部變量就別用全局的變量,全局變量容易引起併發問題。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具有如下優勢:
- ConcurrentSkipListMap的存取速度是ConcurrentSkipListMap的4倍左右
- ConcurrentSkipListMap的key是有序的
- ConcurrentSkipListMap支持更高的併發(它的存取時間和線程數幾乎沒有關係,更高併發的場景下越能體現出優勢)
安全共享對象策略 - 總結
線程限制
:一個被線程限制的對象,由線程獨佔,並且只能被佔有它的線程修改共享只讀
:一個共享只讀的對象,在沒有額外同步的情況下,可以被多個線程併發訪問,但是任何線程都不能修改它線程安全對象
:一個線程安全的對象或者容器,在內部通過同步機制來保證線程安全,所以其他線程無需額外的同步就可以通過公共接口隨意訪問它被守護對象
:被守護對象只能通過獲取特定鎖來訪問