當多個執行緒去訪問某個類時,如果類會表現出我們預期出現的行為,那麼可以稱這個類是執行緒安全的。
什麼時候會出現執行緒不安全?
-
操作並非原子。多個執行緒執行某段程式碼,如果這段程式碼產生的結果受不同執行緒之間的執行時序影響,而產生非預期的結果,即發生了競態條件,就會出現執行緒不安全;
常見場景:
count++
。它本身包含三個操作,讀取、修改、寫入,多執行緒時,由於執行緒執行的時序不同,有可能導致兩個執行緒執行後count只加了1,而原有的目標確實希望每次執行都加1;- 單例。多個執行緒可能同時執行到
instance == null
成立,然後新建了兩個物件,而原有目標是希望這個物件永遠只有一個;
public MyObj getInstance(){ if (instance == null){ instance = new MyObj(); } return instance } 複製程式碼
解決方式是:當前執行緒在操作這段程式碼時,其它執行緒不能對進行操作
常見方案:
- 單個狀態使用 java.util.concurrent.atomic包中的一些原子變數類,注意如果是多個狀態就算每個操作是原子的,複合使用的時候並不是原子的;
- 加鎖。比如使用 synchronized 包圍對應程式碼塊,保證多執行緒之間是互斥的,注意應儘可能的只包含在需要作為原子處理的程式碼塊上;
synchronized的可重入性
當執行緒要去獲取它自己已經持有的鎖是會成功的,這樣的鎖是可重入的,synchronized是可重入的
class Paxi { public synchronized void sayHello(){ System.out.println("hello"); } } class MyClass extends Paxi{ public synchronized void dosomething(){ System.out.println("do thing .."); super.sayHello(); System.out.println("over"); } } 複製程式碼
它的輸出為
do thing .. hello over 複製程式碼
-
修改不可見。讀執行緒無法感知到其它執行緒寫入的值
常見場景:
- 重排序。在沒有同步的情況下,編譯器、處理器以及執行時等都有可能對操作的執行順序進行調整,即寫的程式碼順序和真正的執行順序不一樣,導致讀到的是一個失效的值
- 讀取long、double等型別的變數。JVM允許將一個64位的操作分解成兩個32位的操作,讀寫在不同的執行緒中時,可能讀到錯誤的高低位組合
常見方案:
- 加鎖。所有執行緒都能看到共享變數的最新值;
- 使用Volatile關鍵字宣告變數。只要對這個變數產生了寫操作,那麼所有的讀操作都會看到這個修改;
注意:Volatile並不能保證操作的原子性,比如
count++
操作同樣有風險,它僅保證讀取時返回最新的值。使用的好處在於訪問Volatile變數並不會執行加鎖操作,也就不會阻塞執行緒。
不同步的情況下如何做到執行緒安全?
- 執行緒封閉。即僅在單執行緒內訪問資料,執行緒封閉技術有以下幾種:
- Ad-hoc執行緒封閉。即靠自己寫程式來實現,比如保證程式只在單執行緒上對volatile進行
讀取-修改-寫入
- 棧封閉。所有的操作都反生執行執行緒的棧中,比如在方法中的一個區域性變數
- ThreadLocal類。內部維護了每個執行緒和變數的一個獨立副本
- Ad-hoc執行緒封閉。即靠自己寫程式來實現,比如保證程式只在單執行緒上對volatile進行
- 只讀共享。即使用不可變的物件。
-
使用final去修飾欄位,這樣這個欄位的“值”是不可改變的
注意final如果修飾的是一個物件引用,比如set,它本身包含的值是可變的
-
建立一個不可變的類,來包含多個可變的資料。
class OneValue{ //建立不可變物件,建立之後無法修改,事實上這裡也沒有提供修改的方法 private final BigInteger last; private final BigInteger[] lastfactor; public OneValue(BigInteger i,BigInteger[] lastfactor){ this.last=i; this.lastfactor=Arrays.copy(lastfactor,lastfactor.length); } public BigInteger[] getF(BigInteger i){ if(last==null || !last.equals(i)){ return null; }else{ return Arrays.copy(lastfactor,lastfactor.length) } } } class MyService { //volatile使得cache一經更改,就能被所有執行緒感知到 private volatile OneValue cache=new OneValue(null,null); public void handle(BigInteger i){ BigInteger[] lastfactor=cache.getF(i); if(lastfactor==null){ lastfactor=factor(i); //每次都封裝最新的值 cache=new OneValue(i,lastfactor) } nextHandle(lastfactor) } } 複製程式碼
-
如何構造執行緒安全的類?
-
例項封閉。將一個物件封裝到另一個物件中,這樣能夠訪問被封裝物件的所有程式碼路徑都是已知的,通過合適的加鎖策略可以確保被封裝物件的訪問是執行緒安全的。
java中的Collections.synchronizedList使用的原理就是這樣。部分程式碼為
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } 複製程式碼
SynchronizedList的實現,注意此處用到的mutex是內建鎖
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { private static final long serialVersionUID = -7754090372962971524L; final List<E> list; public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } } 複製程式碼
mutex的實現
static class SynchronizedCollection<E> implements Collection<E>, >Serializable { private static final long serialVersionUID = 3053995032091335093L; final Collection<E> c; // Backing Collection final Object mutex; // Object on which to synchronize SynchronizedCollection(Collection<E> c) { if (c==null) throw new NullPointerException(); this.c = c; mutex = this; // mutex實際上就是物件本身 } 複製程式碼
什麼是監視器模式
java的監視器模式,將物件所有可變狀態都封裝起來,並由物件自己的內建鎖來保護,即是一種例項封閉。比如HashTable就是運用的監視器模式。它的get操作就是用的synchronized,內建鎖,來實現的執行緒安全
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null; } 複製程式碼
-
內建鎖
每個物件都有內建鎖。內建鎖也稱為監視器鎖。或者可以簡稱為監視器
執行緒執行一個物件的用synchronized修飾的方法時,會自動的獲取這個物件的內建鎖,方法返回時自動釋放內建鎖,執行過程中就算丟擲異常也會自動釋放。
以下兩種寫法等效:synchronized void myMethdo(){ //do something } void myMethdo(){ synchronized(this){ //do somthding } } 複製程式碼
-
私有鎖
public class PrivateLock{ private Object mylock = new Object(); //私有鎖 void myMethod(){ synchronized(mylock){ //do something } } } 複製程式碼
它也可以用來保護物件,相對內建鎖,優勢在於私有鎖可以有多個,同時可以讓客戶端程式碼顯示的獲取私有鎖
-
類鎖
在staic方法上修飾的,一個類的所有物件共用一把鎖
-
-
把執行緒安全性委託給執行緒安全的類
如果一個類中的各個元件都是執行緒安全的,該類是否要處理執行緒安全問題?
視情況而定。
-
只有單個元件,且它是執行緒安全的。
public class DVT{ private final ConcurrentMap<String,Point> locations; private final Map<String,Point> unmodifiableMap; public DVT(Map<String,Point> points){ locations=new ConcurrentHashMap<String,Point>(points); unmodifiableMap=Collections.unmodifiableMap(locations); } public Map<String,Point> getLocations(){ return unmodifiableMap; } public Point getLocation(String id){ return locations.get(id); } public void setLocation(String id,int x,int y){ if(locations.replace(id,new Point(x,y))==null){ throw new IllegalArgumentException("invalid "+id); } } } public class Point{ public final int x,y; public Point(int x,int y){ this.x=x; this.y=y; } } 複製程式碼
執行緒安全性分析
- Point類本身是無法更改的,所以它是執行緒安全的,DVT返回的Point方法也是執行緒安全的
- DVT的方法getLocations返回的物件是不可修改的,是執行緒安全的
- setLocation實際操作的是ConcurrentHashMap它也是執行緒安全的
綜上,DVT的安全交給了‘locations’,它本身是執行緒安全的,DVT本身雖沒有任何顯示的同步,也是執行緒安全。這種情況下,就是DVT的執行緒安全實際是委託給了‘locations’,整個DVT表現出了執行緒安全。
-
執行緒安全性委託給了多個狀態變數
只要多個狀態變數之間彼此獨立,組合的類並不會在其包含的多個狀態變數上增加不變性。依賴的增加則無法保證執行緒安全
public class NumberRange{ private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i){ //先檢查後執行,存在隱患 if (i>upper.get(i)){ throw new IllegalArgumentException('can not ..'); } lower.set(i); } public void setUpper(int i){ //先檢查後執行,存在隱患 if(i<lower.get(i)){ throw new IllegalArgumentException('can not ..'); } upper.set(i); } } 複製程式碼
setLower和setUpper都是‘先檢查後執行’的操作,但是沒有足夠的加鎖機制保證操作的原子性。假設原始範圍是(0,10),一個執行緒呼叫 setLower(5),一個設定setUpper(4)錯誤的執行時序將可能導致結果為(5,4)
如何對現有的執行緒安全類進行擴充套件?
假設需要擴充套件的功能為 ‘沒有就新增’。
- 直接修改原有的程式碼。但通常沒有辦法修改原始碼
- 繼承。繼承原有的程式碼,新增新的功能。但是同步策略儲存在兩份檔案中,如果底層同步策略變更,很容易出問題
- 組合。將類放入一個輔助類中,通過輔助類的操作程式碼。
比如擴充套件 Collections.synchronizedList。期間需要注意鎖的機制,錯誤方式為
這裡的putIfAbsent並不能帶來執行緒安全,原因是list的內建鎖並不是ListHelper,也就是putIfAbsent相對list的其它方法並不是原子的。Collections.synchronizedList是鎖在list本身的,正確方式為public class ListHelper<E>{ public List<E> list=Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } } 複製程式碼
public boolean putIfAbsent(E x){ synchronized(list){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } } 複製程式碼
另外可以不管要操作的類是否是執行緒安全,對類統一新增一層額外的鎖。 實現參考Collections.synchronizedList方法