安全執行緒策略
1. 不可變對象
1.1 滿足條件 對象建立以後其狀態就不能改變 對象所有域都是final類型 對象是正確建立的(在對象建立期間,this引用沒有逸出,即將類聲明為final,將成員全部聲明為私人成員,對變數不停set方法,參考String類型)
2. 聲明不可變對象
2.1 Collections.unmodifiableXXX
它的主要實現是將現在的集合對象傳入到Collections.unmodifiableXXX方法中
通過:
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) { return new UnmodifiableMap<>(m);}
返回一個自訂的UnmodifiableMap對象,此方法允許模組為使用者提供對內部映射的“唯讀”訪問。在返回的映射上執行的查詢操作將“讀完”指定的映射。試圖修改返回的映射(不管是直接修改還是通過其 collection 視圖進行修改)將導致拋出 UnsupportedOperationException。
Collection、List、Set、Map等 我們只需要把要聲明的集合類型對象傳入到這個方法裡面,這個集合類型就不可以被修改了。
import com.liuyao.concurrency.annotations.TreadSafe;import java.util.Collections;import java.util.HashMap;import java.util.Map;/** * Created By liuyao on 2018/4/17 17:15. */@TreadSafepublic class ImmutableExample2 { private static Map<Integer,Integer> map=new HashMap<>(); static { map.put(1,2); map.put(2,3); map= Collections.unmodifiableMap(map); } public static void main(String[] args) { map.put(1,3); //將會拋出異常 System.out.println(map.get(1)); }}
2.2 ImmutableXXX
Guava:Collection、List、Set、Map 提供了帶初始化資料的方法,一旦初始化完成就不可以被修改了。
package com.liuyao.concurrency.example.immutable;import com.google.common.collect.ImmutableList;import com.google.common.collect.ImmutableMap;import com.google.common.collect.ImmutableSet;import com.liuyao.concurrency.annotations.ThreadSafe;/** * Created By liuyao on 2018/4/17 17:33. */@ThreadSafepublic class ImmutableExample3 { private final static ImmutableList list=ImmutableList.of(1,2,3); private final static ImmutableSet set=ImmutableSet.copyOf(list); private final static ImmutableMap<Integer,Integer> map=ImmutableMap.of(1,2,3,4,5,6); private final static ImmutableMap<Integer,Integer> map2=ImmutableMap.<Integer,Integer>builder().put(1,2).put(3,4).build(); public static void main(String[] args) {// list.add(4);// map.put(1,3); System.out.println(map.get(2)); }}
3. 線程封閉 Ad-hoc線程封閉:程式控制實現,最糟糕的,忽略 堆棧封閉:局部變數,無並發問題 ThreadLocal線程封閉:特別好的封閉方法
4. 線程不安全類和寫法
StringBuilder:線程不安全,方法內部使用,堆棧封閉
StringBuffer:安全執行緒的,用了Synchronized,效能較差 4.1 SimpleDateFormat
不是安全執行緒的,在多線程的情況下,將會出現異常。
多個線程共用一個,線程不安全:
private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd"); private static void update(){ try { simpleDateFormat.parse("2018-04-19"); } catch (ParseException e) { e.printStackTrace(); } }
每次申請一個新的使用,安全執行緒:
private static void update(){ SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd"); try { simpleDateFormat.parse("2018-04-19"); } catch (ParseException e) { e.printStackTrace(); }}
4.2 JodaTime:安全執行緒的
pom.xml
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9</version> </dependency>
用法:
private static DateTimeFormatter dateTimeFormatter= DateTimeFormat.forPattern("yyyy-MM-dd");private static void update(){ DateTime.parse("2018-04-19",dateTimeFormatter).toDate();}
5. 同步容器
ArrayList HashSet HashMap都是線程不安全的 5.1 ArrayList -> Vector,Stack
對於Vector雖然兩個方法都是Synchronized的,但也可能存線上程不安全的情況
import java.util.Vector;/** * Created By liuyao on 2018/4/20 18:00. */public class VectorExample2 { private static Vector<Integer> vector=new Vector<>(); public static void main(String[] args) { while (true){ for (int i = 0; i < 10; i++) { vector.add(i); } Thread thread1=new Thread(){ @Override public void run() { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }; Thread thread2=new Thread(){ @Override public void run() { for (int i = 0; i < vector.size(); i++) { vector.get(i); } } }; thread1.start(); thread2.start(); } }}
上訴兩個方法執行時,由於get方法在取值的時候,很有可能另一個線程的remove方法已經移除該元素的了,故拋出了異常。兩個線程執行順序的差異導致的同步問題。 5.2 HashMap -> HashTable(key,value不能為null) #### 5.3 Collections.synchronizedXXX(List,Map,Set)
private static List<Integer> list= Collections.synchronizedList(new ArrayList<Integer>());
5.4 集合邊遍曆邊刪除
import com.liuyao.concurrency.annotations.NotThreadSafe;import java.util.Iterator;import java.util.Vector;/** * Created By liuyao on 2018/4/20 18:00. */@NotThreadSafepublic class VectorExample3 { //java.util.ConcurrentModificationException public static void test1(Vector<Integer> v1){ for (Integer i :v1) { if (i.equals(3)){ v1.remove(i); } } }// java.util.ConcurrentModificationException public static void test2(Vector<Integer> v2){ Iterator<Integer> iterator=v2.iterator(); while (iterator.hasNext()){ Integer i=iterator.next(); if (i.equals(3)){ v2.remove(i); } } }// success public static void test3(Vector<Integer> v3){ for (int i = 0; i < v3.size(); i++) { if (v3.get(i).equals(3)){ v3.remove(i); } } } public static void main(String[] args) { Vector<Integer> vector=new Vector<>(); vector.add(1); vector.add(2); vector.add(3); test3(vector); }}
當集合遍曆是對集合進行remove操作會導致:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
上面的modCount和expectedModCount不相等,拋出異常,所以使用了foreach迴圈和迭代器迴圈,盡量不要在早操作過程中進行remove操作,可以先標記等遍曆完再進行remove操作。單線程都會出錯,多線程情況下,出現錯誤的情況更大。 6. 並發容器 J.U.C 6.1 ArrayList -> CopyOnWriteArrayList
private static List<Integer> arrayList=new CopyOnWriteArrayList<Integer>();
寫操作複製,當有新的元素添加到數組中時,先從原有的數組中拷貝一份出來,在新的數組中進行寫操作,然後將原來的數組指向新的數組上。整個寫操作都是在鎖的保護下進行的,主要是為了多線程並發時,複製多個數組出來把數組搞亂了。
因為寫操作要複製元素,如果元素較多的情況下,可能會導致GC,不能用於即時讀的情境,由於拷貝數組和新增元素都需要時間,所以不能滿足即時性,但能滿足最後的結果是正確的。
CopyOnWriteArrayList適合讀多寫少的情況。當你無法保證CopyOnWriteArrayList到底要放置多少資料時,建議不要使用,因為每次更新操作,都會複製操作,這個代價有點大,可能會引起效能故障。
三個思想:
1. 讀寫分離,讀不需要加鎖,寫要加鎖
2. 最終一致性,保證最終是對的
3. 另外開闢空間來解決並發衝突 6.2 HashSet,TreeSet -> CopyOnWriteArraySet,ConcurrentSkipListSet
CopyOnWriteArraySet底層實現是用了CopyOnWriteArrayList,適用於大小很小的set,讀操作大於寫操作。add,set,remove操作開銷較大。
ConcurrentSkipListSet是JDK6新增的類,是自然排序的,並且可以自訂比較子,基於Map集合的額,多線程情況下對於add,remove,contains是安全執行緒的,對addAll,removeAll,containsAll不是同步的,他們底層調用的還是上面的這些單個方法,大量操作時只能一次的是執行緒安全性的,需要自己手動加鎖。
ConcurrentSkipListSet不允許使用空元素,它無法可靠的將元素的傳回值與不存在的元素區分開來。 6.3 HashMap,TreeMap -> ConcurrentHashMap,ConcurrentSkipListMap
ConcurrentHashMap不允許有空值,對讀做了大量最佳化
ConcurrentSkipListMap的key有序,支援更高的並發,存取時間和線程數沒關係,並發線程數越多,效果越明顯。
7. 安全共用對象策略-總結 線程限制:一個被線程限制的對象,有線程獨佔,並且只能被佔有它的線程修改 共用唯讀:一個共用唯讀對象,在沒有額外同步情況下,可以被多個線程並發訪問,但是任何線程都不能修改它 安全執行緒對象:一個安全執行緒的對象或者容器,在內部通過同步機制來保證安全執行緒,所以其他線程無需額外的同步就可以通過公用介面隨意訪問它 被守護對象:被守護對象只能通過擷取特定的鎖來訪問。