四個(gè)線程安全策略
1、線程限制:
- 一個(gè)被線程限制的對(duì)象,由線程獨(dú)占,并且只能被占有它的線程修改
2、共享只讀:
- 一個(gè)共享只讀的對(duì)象,在沒(méi)有額外同步的情況下,可以被多個(gè)線程并發(fā)訪問(wèn),但是任何線程都不能修改它
3、線程安全對(duì)象:
- 一個(gè)線程安全的對(duì)象或者容器,在內(nèi)部通過(guò)同步機(jī)制來(lái)保證線程安全,所以其他線程無(wú)需額外的同步就可以通過(guò)公共接口隨意訪問(wèn)它
4、被守護(hù)對(duì)象:
- 被守護(hù)對(duì)象只能通過(guò)獲取特定的鎖來(lái)訪問(wèn)
不可變對(duì)象
有一種對(duì)象發(fā)布了就是安全的,這就是不可變對(duì)象,本小節(jié)簡(jiǎn)單介紹一下不可變對(duì)象。不可變對(duì)象可以在多線程在保證線程安全,不可變對(duì)象需要滿(mǎn)足的條件:
- 對(duì)象創(chuàng)建以后其狀態(tài)就不能修改
- 對(duì)象所有域都是final類(lèi)型
- 對(duì)象是正確創(chuàng)建的(在對(duì)象創(chuàng)建期間,this引用沒(méi)有逸出)
創(chuàng)建不可變對(duì)象的方式(參考String):
- 將類(lèi)聲明成final類(lèi)型,使其不可以被繼承
- 將所有的成員設(shè)置成私有的,使其他的類(lèi)和對(duì)象不能直接訪問(wèn)這些成員
- 對(duì)變量不提供set方法
- 將所有可變的成員聲明為final,這樣只能對(duì)他們賦值一次
- 通過(guò)構(gòu)造器初始化所有成員,進(jìn)行深度拷貝
- 在get方法中,不直接返回對(duì)象本身,而是克隆對(duì)象,返回對(duì)象的拷貝
提到不可變的對(duì)象就不得不說(shuō)一下final關(guān)鍵字,該關(guān)鍵字可以修飾類(lèi)、方法、變量:
- 修飾類(lèi):不能被繼承(final類(lèi)中的所有方法都會(huì)被隱式的聲明為final方法)
- 修飾方法:
- 1、鎖定方法不被繼承類(lèi)修改;
- 2、可提升效率(private方法被隱式修飾為final方法)
- 修飾變量:基本數(shù)據(jù)類(lèi)型變量(初始化之后不能修改)、引用類(lèi)型變量(初始化之后不能再修改其引用)
- 修飾方法參數(shù):同修飾變量
通常我們會(huì)使用一些工具類(lèi)來(lái)完成不可變對(duì)象的創(chuàng)建:
- Collections.unmodifiableXXX:Collection、List、Set、Map...
- Guava:ImmutableXXX:Collection、List、Set、Map...
由于這些工具類(lèi)的存在,所以我們創(chuàng)建不可變對(duì)象并不是很費(fèi)勁,而且其實(shí)現(xiàn)源碼也不會(huì)很難懂。所以如果需要自定義不可變對(duì)象,也可以參考這些工具類(lèi)的實(shí)現(xiàn)源碼去進(jìn)行實(shí)現(xiàn)。接下來(lái)我們看一下如何使用Collections.unmodifiableXXX方法將map轉(zhuǎn)換為一個(gè)不可變的對(duì)象,代碼如下:
@Slf4j
public class ImmutableExample2 {
private static Map<Integer, Integer> map = Maps.newHashMap();
static {
map.put(1, 2);
// 轉(zhuǎn)換成不可變對(duì)象
map = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
// 此時(shí)map就是不可變對(duì)象了,修改會(huì)報(bào)錯(cuò)
map.put(1, 3);
log.info("{}", map.get(1));
}
}
我們來(lái)看看是如何將map轉(zhuǎn)換為不可變對(duì)象的,源碼如下:
/**
* Returns an <a href="Collection.html#unmodview">unmodifiable view</a> of the
* specified map. Query operations on the returned map "read through"
* to the specified map, and attempts to modify the returned
* map, whether direct or via its collection views, result in an
* {@code UnsupportedOperationException}.<p>
*
* The returned map will be serializable if the specified map
* is serializable.
*
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map for which an unmodifiable view is to be returned.
* @return an unmodifiable view of the specified map.
*/
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
return new UnmodifiableMap<>(m);
}
/**
* @serial include
*/
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
private static final long serialVersionUID = -1034234728574286014L;
private final Map<? extends K, ? extends V> m;
UnmodifiableMap(Map<? extends K, ? extends V> m) {
if (m==null)
throw new NullPointerException();
this.m = m;
}
public int size() {return m.size();}
public boolean isEmpty() {return m.isEmpty();}
public boolean containsKey(Object key) {return m.containsKey(key);}
public boolean containsValue(Object val) {return m.containsValue(val);}
public V get(Object key) {return m.get(key);}
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
public V remove(Object key) {
throw new UnsupportedOperationException();
}
...
可以看到,實(shí)際上unmodifiableMap方法里是返回了一個(gè)內(nèi)部類(lèi)UnmodifiableMap的實(shí)例。而這個(gè)UnmodifiableMap類(lèi)實(shí)現(xiàn)了Map接口,并且在構(gòu)造器中將我們傳入的map對(duì)象賦值到了final修飾的屬性m中。在該類(lèi)中除了一些“查詢(xún)”方法,其他涉及到修改的方法都會(huì)拋出UnsupportedOperationException異常,這樣外部就無(wú)法修改該對(duì)象內(nèi)的數(shù)據(jù)。我們?cè)谡{(diào)用涉及到修改數(shù)據(jù)的方法都會(huì)報(bào)錯(cuò),這樣就實(shí)現(xiàn)了將一個(gè)可變對(duì)象轉(zhuǎn)換成一個(gè)不可變的對(duì)象。
除了以上示例中所使用的unmodifiableMap方法外,還有許多轉(zhuǎn)換不可變對(duì)象的方法,如下:

然后我們?cè)賮?lái)看看Guava中創(chuàng)建不可變對(duì)象的方法,示例代碼如下:
@Slf4j
public class ImmutableExample3 {
/**
* 不可變的list
*/
private final static List<Integer> list = ImmutableList.of(1, 2, 3);
/**
* 不可變的set
*/
private final static Set<Integer> set = ImmutableSet.copyOf(list);
/**
* 不可變的map,需要以k/v的形式傳入數(shù)據(jù),即奇數(shù)位參數(shù)為key,偶數(shù)位參數(shù)為value
*/
private final static Map<Integer, Integer> map = ImmutableMap.of(1, 1, 2, 2, 3, 3);
/**
* 通過(guò)builder調(diào)用鏈的方式構(gòu)造不可變的map
*/
private final static Map<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder()
.put(1, 1).put(2, 2).put(3, 3).build();
public static void main(String[] args) {
// 修改對(duì)象內(nèi)的數(shù)據(jù)就會(huì)拋出UnsupportedOperationException異常
list.add(4);
set.add(5);
map.put(1, 2);
map2.put(1, 2);
}
}
不可變對(duì)象的概念也比較簡(jiǎn)單,又有那么多的工具類(lèi)可供使用,所以學(xué)習(xí)起來(lái)也不是很困難。由于Guava中實(shí)現(xiàn)不可變對(duì)象的方式和Collections差不多,所以這里就不對(duì)其源碼進(jìn)行介紹了。
線程封閉
在上一小節(jié)中,我們介紹了不可變對(duì)象,不可變對(duì)象在多線程下是線程安全的,因?yàn)槠浔荛_(kāi)了并發(fā),而另一個(gè)更簡(jiǎn)單避開(kāi)并發(fā)的的方式就是本小節(jié)要介紹的線程封閉。
線程封閉最常見(jiàn)的應(yīng)用就是用在數(shù)據(jù)庫(kù)連接對(duì)象上,數(shù)據(jù)庫(kù)連接對(duì)象本身并不是線程安全的,但由于線程封閉的作用,一個(gè)線程只會(huì)持有一個(gè)連接對(duì)象,并且持有的連接對(duì)象不會(huì)被其他線程所獲取,這樣就不會(huì)有線程安全的問(wèn)題了。
線程封閉概念:
- 把對(duì)象封裝到一個(gè)線程里,只有這個(gè)線程能看到這個(gè)對(duì)象。那么即便這個(gè)對(duì)象本身不是線程安全的,但由于線程封閉的關(guān)系讓其只能在一個(gè)線程里訪問(wèn),所以也就不會(huì)出現(xiàn)線程安全的問(wèn)題了
實(shí)現(xiàn)線程封閉的方式:
- Ad-hoc 線程封閉:完全由程序控制實(shí)現(xiàn),最糟糕的方式,忽略
- 堆棧封閉:局部變量,當(dāng)多個(gè)線程訪問(wèn)同一個(gè)方法的時(shí)候,方法內(nèi)的局部變量都會(huì)被拷貝一份副本到線程的棧中,所以局部變量是不會(huì)被多個(gè)線程所共享的,因此無(wú)并發(fā)問(wèn)題。所以我們?cè)陂_(kāi)發(fā)時(shí)應(yīng)盡量使用局部變量而不是全局變量
- ThreadLocal 線程封閉:每個(gè)Thread線程內(nèi)部都有個(gè)map,這個(gè)map是以線程本地對(duì)象作為key,以線程的變量副本作為value。而這個(gè)map是由ThreadLocal來(lái)維護(hù)的,由ThreadLocal負(fù)責(zé)向map里設(shè)置線程的變量值,以及獲取值。所以對(duì)于不同的線程,每次獲取副本值的時(shí)候,其他線程都不能獲取當(dāng)前線程的副本值,于是就形成了副本的隔離,多個(gè)線程互不干擾。所以這是特別好的實(shí)現(xiàn)線程封閉的方式
ThreadLocal 應(yīng)用的場(chǎng)景也比較多,例如在經(jīng)典的web項(xiàng)目中,都會(huì)涉及到用戶(hù)的登錄。而服務(wù)器接收到每個(gè)請(qǐng)求都是開(kāi)啟一個(gè)線程去處理的,所以我們通常會(huì)使用ThreadLocal存儲(chǔ)登錄的用戶(hù)信息對(duì)象,這樣我們就可以方便的在該請(qǐng)求生命周期內(nèi)的任何位置獲取到用戶(hù)對(duì)象,并且不會(huì)有線程安全問(wèn)題。示例代碼如下:
@Slf4j
public class RequestHolder {
private final static ThreadLocal<Long> REQUEST_HOLDER = new ThreadLocal<>();
/**
* 添加數(shù)據(jù)
*
* @param id id
*/
public static void add(User user) {
// ThreadLocal 內(nèi)部維護(hù)一個(gè)map,key為當(dāng)前線程id,value為當(dāng)前set的變量
REQUEST_HOLDER.set(user);
}
/**
* 會(huì)通過(guò)當(dāng)前線程id獲取數(shù)據(jù)
*
* @return id
*/
public static Long getId() {
return REQUEST_HOLDER.get();
}
/**
* 移除變量信息
* 如果不移除,那么變量不會(huì)釋放掉,會(huì)造成內(nèi)存泄漏
*/
public static void remove() {
REQUEST_HOLDER.remove();
}
}
常見(jiàn)的線程不安全的類(lèi)與寫(xiě)法
所謂線程不安全的類(lèi),是指該類(lèi)的實(shí)例對(duì)象可以同時(shí)被多個(gè)線程共享訪問(wèn),如果不做同步或線程安全的處理,就會(huì)表現(xiàn)出線程不安全的行為。
1.字符串拼接,在Java里提供了兩個(gè)類(lèi)可完成字符串拼接,就是StringBuilder和StringBuffer,其中StringBuilder是線程不安全的,而StringBuffer是線程安全的
StringBuffer之所以是線程安全的原因是幾乎所有的方法都加了synchronized關(guān)鍵字,所以是線程安全的。但是由于StringBuffer 是以加 synchronized 這種暴力的方式保證的線程安全,所以性能會(huì)相對(duì)較差,在堆棧封閉等線程安全的環(huán)境下應(yīng)該首先選用StringBuilder。
2.SimpleDateFormat
SimpleDateFormat 的實(shí)例對(duì)象在多線程共享使用的時(shí)候會(huì)拋出轉(zhuǎn)換異常,正確的使用方法應(yīng)該是采用堆棧封閉,將其作為方法內(nèi)的局部變量而不是全局變量,在每次調(diào)用方法的時(shí)候才去創(chuàng)建一個(gè)SimpleDateFormat實(shí)例對(duì)象,這樣利于堆棧封閉就不會(huì)出現(xiàn)并發(fā)問(wèn)題。另一種方式是使用第三方庫(kù)joda-time的DateTimeFormatter類(lèi)(推薦使用)
錯(cuò)誤寫(xiě)法:
@Slf4j
public class DateFormatExample1 {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private static void parse() {
try {
// 多線程訪問(wèn)下會(huì)報(bào)錯(cuò)
simpleDateFormat.parse("2018-02-08");
} catch (ParseException e) {
log.error("ParseException", e);
}
}
}
正確寫(xiě)法:
@Slf4j
public class DateFormatExample1 {
private static void parse() {
try {
// 在多線程下使用SimpleDateFormat的正確方式,利用堆棧封閉特性
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
simpleDateFormat.parse("2018-02-08");
} catch (ParseException e) {
log.error("ParseException", e);
}
}
}
推薦使用DateTimeFormatter類(lèi):
@Slf4j
public class DateFormatExample3 {
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
private static void parse() {
Date date = DateTime.parse("2018-02-08", dateTimeFormatter).toDate();
}
}
3.ArrayList, HashMap, HashSet 等 Collections 都是線程不安全的
4.有一種寫(xiě)法需要注意,即便是線程安全的對(duì)象,在這種寫(xiě)法下也可能會(huì)出現(xiàn)線程不安全的行為,這種寫(xiě)法就是先檢查后執(zhí)行:
if(condition(a)){
handle(a);
}
在這個(gè)操作里,可能會(huì)有兩個(gè)線程同時(shí)通過(guò)if的判斷,然后去執(zhí)行了處理方法,那么就會(huì)出現(xiàn)兩個(gè)線程同時(shí)操作一個(gè)對(duì)象,從而出現(xiàn)線程不安全的行為。這種寫(xiě)法導(dǎo)致線程不安全的主要原因是因?yàn)檫@里分成了兩步操作,這個(gè)過(guò)程是非原子性的,所以就會(huì)出現(xiàn)線程不安全的問(wèn)題。
同步容器簡(jiǎn)介
在上一小節(jié)中,我們提到了一些常用的線程不安全的集合容器,當(dāng)我們?cè)谑褂眠@些容器時(shí),需要自行處理線程安全問(wèn)題。所以使用起來(lái)相對(duì)會(huì)有些不便,而Java在這方面提供了相應(yīng)的同步容器,我們可以在多線程情況下可以結(jié)合實(shí)際場(chǎng)景考慮使用這些同步容器。
1.在Java中同步容器主要分為兩類(lèi),一類(lèi)是集合接口下的同步容器實(shí)現(xiàn)類(lèi):
- List -> Vector、Stack
- Map -> HashTable(key、value不能為null)
注:vector的所有方法都是有synchronized關(guān)鍵字保護(hù)的,stack繼承了vector,并且提供了棧操作(先進(jìn)后出),而hashtable也是由synchronized關(guān)鍵字保護(hù)
但是需要注意的是同步容器也并不一定是絕對(duì)線程安全的,例如有兩個(gè)線程,線程A根據(jù)size的值循環(huán)執(zhí)行remove操作,而線程B根據(jù)size的值循環(huán)執(zhí)行執(zhí)行g(shù)et操作。它們都需要調(diào)用size獲取容器大小,當(dāng)循環(huán)到最后一個(gè)元素時(shí),若線程A先remove了線程B需要get的元素,那么就會(huì)報(bào)越界錯(cuò)誤。錯(cuò)誤示例如下:
@Slf4j
public class VectorExample2 {
private static List<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Runnable thread1 = () -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
};
Runnable thread2 = () -> {
for (int i = 0; i < vector.size(); i++) {
// 當(dāng)thread2想獲取i=9的元素的時(shí)候,而thread1剛好將i=9的元素移除了,就會(huì)導(dǎo)致數(shù)組越界
vector.get(i);
}
};
new Thread(thread1).start();
new Thread(thread2).start();
}
}
}
另外還有一點(diǎn)需要注意的是,當(dāng)我們使用foreach循環(huán)或迭代器去遍歷元素的同時(shí)又執(zhí)行刪除操作的話,即便在單線程下也會(huì)報(bào)并發(fā)修改異常。示例代碼如下:
public class VectorExample3 {
private static void test1(Vector<Integer> v1) {
// 在遍歷的同時(shí)進(jìn)行了刪除的操作,會(huì)拋出java.util.ConcurrentModificationException并發(fā)修改異常
for (Integer integer : v1) {
if (integer.equals(5)) {
v1.remove(integer);
}
}
}
private static void test2(Vector<Integer> v1) {
// 在遍歷的同時(shí)進(jìn)行了刪除的操作,會(huì)拋出java.util.ConcurrentModificationException并發(fā)修改異常
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
if (integer.equals(5)) {
v1.remove(integer);
}
}
}
private static void test3(Vector<Integer> v1) {
// 可以正常刪除
for (int i = 0; i < v1.size(); i++) {
if (v1.get(i).equals(5)) {
v1.remove(i);
}
}
}
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
for (int i = 1; i <= 5; i++) {
vector.add(i);
}
test1(vector);
// test2(vector);
// test3(vector);
}
}
所以在foreach循環(huán)或迭代器遍歷的過(guò)程中不能做刪除操作,若需遍歷的同時(shí)進(jìn)行刪除操作的話盡量使用for循環(huán)。實(shí)在要使用foreach循環(huán)或迭代器的話應(yīng)該先標(biāo)記要?jiǎng)h除元素的下標(biāo),然后最后再統(tǒng)一刪除。如下示例:
private static void test4(Vector<Integer> v1) {
int delIndex = 0;
for (Integer integer : v1) {
if (integer.equals(5)) {
delIndex = v1.indexOf(integer);
}
}
v1.remove(delIndex);
}
最方便的方式就是使用jdk1.8提供的函數(shù)式編程接口:
private static void test5(Vector<Integer> v1){
v1.removeIf((i) -> i.equals(5));
}
2.第二類(lèi)是Collections.synchronizedXXX (list,set,map)方法所創(chuàng)建的同步容器
示例代碼:
public class CollectionsExample{
private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
private static Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
private static Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());
}
并發(fā)容器簡(jiǎn)介
在上一小節(jié)中,我們簡(jiǎn)單介紹了常見(jiàn)的同步容器,知道了同步容器是通過(guò)synchronized來(lái)實(shí)現(xiàn)同步的,所以性能較差。而且同步容器也并不是絕對(duì)線程安全的,在一些特殊情況下也會(huì)出現(xiàn)線程不安全的行為。那么有沒(méi)有更好的方式代替同步容器呢?答案是有的,那就是并發(fā)容器,有了并發(fā)容器后同步容器的使用也越來(lái)越少的,大部分都會(huì)優(yōu)先使用并發(fā)容器。本小節(jié)將簡(jiǎn)單介紹一下并發(fā)容器,并發(fā)容器也稱(chēng)為J.U.C,即是其包名:java.util.concurrent。
1.ArrayList對(duì)應(yīng)的CopyOnWriteArrayList:
CopyOnWrite容器即寫(xiě)時(shí)復(fù)制的容器。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個(gè)新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對(duì)CopyOnWrite容器進(jìn)行并發(fā)的讀,而不需要加鎖,因?yàn)楫?dāng)前容器不會(huì)添加任何元素。所以CopyOnWrite容器也是一種讀寫(xiě)分離的思想,讀和寫(xiě)不同的容器。而在CopyOnWriteArrayList寫(xiě)的過(guò)程是會(huì)加鎖的,即調(diào)用add的時(shí)候,否則多線程寫(xiě)的時(shí)候會(huì)Copy出N個(gè)副本出來(lái)。
CopyOnWriteArrayList.add()方法源碼如下(jdk11版本):
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//1、先加鎖
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
//2、拷貝數(shù)組
es = Arrays.copyOf(es, len + 1);
//3、將元素加入到新數(shù)組中
es[len] = e;
//4、將array引用指向到新數(shù)組
setArray(es);
return true;
}
}
讀的時(shí)候不需要加鎖,但是如果讀的時(shí)候有多個(gè)線程正在向CopyOnWriteArrayList添加數(shù)據(jù),讀還是會(huì)讀到舊的數(shù)據(jù),因?yàn)閷?xiě)的時(shí)候不會(huì)鎖住舊的CopyOnWriteArrayList。CopyOnWriteArrayList.get()方法源碼如下(jdk11版本):
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return elementAt(getArray(), index);
}
CopyOnWriteArrayList容器有很多優(yōu)點(diǎn),但是同時(shí)也存在兩個(gè)問(wèn)題,即內(nèi)存占用問(wèn)題和數(shù)據(jù)一致性問(wèn)題:
- 內(nèi)存占用問(wèn)題:
因?yàn)镃opyOnWriteArrayList的寫(xiě)操作時(shí)的復(fù)制機(jī)制,所以在進(jìn)行寫(xiě)操作的時(shí)候,內(nèi)存里會(huì)同時(shí)駐扎兩個(gè)對(duì)象的內(nèi)存,舊的對(duì)象和新寫(xiě)入的對(duì)象(注意:在復(fù)制的時(shí)候只是復(fù)制容器里的引用,只是在寫(xiě)的時(shí)候會(huì)創(chuàng)建新對(duì)象添加到新容器里,而舊容器的對(duì)象還在使用,所以有兩份對(duì)象內(nèi)存)。如果這些對(duì)象占用的內(nèi)存比較大,比如說(shuō)200M左右,那么再寫(xiě)入100M數(shù)據(jù)進(jìn)去,內(nèi)存就會(huì)占用300M,那么這個(gè)時(shí)候很有可能造成頻繁的Yong GC和Full GC。之前我們系統(tǒng)中使用了一個(gè)服務(wù)由于每晚使用CopyOnWrite機(jī)制更新大對(duì)象,造成了每晚15秒的Full GC,應(yīng)用響應(yīng)時(shí)間也隨之變長(zhǎng)。
針對(duì)內(nèi)存占用問(wèn)題,可以通過(guò)壓縮容器中的元素的方法來(lái)減少大對(duì)象的內(nèi)存消耗,比如,如果元素全是10進(jìn)制的數(shù)字,可以考慮把它壓縮成36進(jìn)制或64進(jìn)制。或者不使用CopyOnWrite容器,而使用其他的并發(fā)容器,如ConcurrentHashMap。
- 數(shù)據(jù)一致性問(wèn)題:
CopyOnWriteArrayList容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫(xiě)入的的數(shù)據(jù),馬上能讀到,即實(shí)時(shí)讀取場(chǎng)景,那么請(qǐng)不要使用CopyOnWriteArrayList容器。
CopyOnWrite的應(yīng)用場(chǎng)景:
綜上,CopyOnWriteArrayList并發(fā)容器用于讀多寫(xiě)少的并發(fā)場(chǎng)景。不過(guò)這類(lèi)慎用因?yàn)檎l(shuí)也沒(méi)法保證CopyOnWriteArrayList 到底要放置多少數(shù)據(jù),萬(wàn)一數(shù)據(jù)稍微有點(diǎn)多,每次add/set都要重新復(fù)制數(shù)組,這個(gè)代價(jià)實(shí)在太高昂了。在高性能的互聯(lián)網(wǎng)應(yīng)用中,這種操作分分鐘引起故障。
參考文章:
2.HashSet對(duì)應(yīng)的CopyOnWriteArraySet
CopyOnWriteArraySet是線程安全的,它底層的實(shí)現(xiàn)使用了CopyOnWriteArrayList,因此和CopyOnWriteArrayList概念是類(lèi)似的。使用迭代器進(jìn)行遍歷的速度很快,并且不會(huì)與其他線程發(fā)生沖突。在構(gòu)造迭代器時(shí),迭代器依賴(lài)于不可變的數(shù)組快照,所以迭代器不支持可變的 remove 操作。
CopyOnWriteArraySet適合于具有以下特征的場(chǎng)景:
- set 大小通常保持很小,只讀操作遠(yuǎn)多于可變操作,需要在遍歷期間防止線程間的沖突。
CopyOnWriteArraySet缺點(diǎn):
- 因?yàn)橥ǔP枰獜?fù)制整個(gè)基礎(chǔ)數(shù)組,所以可變操作(add、set 和 remove 等等)的開(kāi)銷(xiāo)很大。
3.TreeSet對(duì)應(yīng)的ConcurrentSkipListSet
ConcurrentSkipListSet是jdk6新增的類(lèi),它和TreeSet一樣是支持自然排序的,并且可以在構(gòu)造的時(shí)候定義Comparator<E> 的比較器,該類(lèi)的方法基本和TreeSet中方法一樣(方法簽名一樣)。和其他的Set集合一樣,ConcurrentSkipListSet是基于Map集合的,ConcurrentSkipListMap便是它的底層實(shí)現(xiàn)
在多線程的環(huán)境下,ConcurrentSkipListSet中的contains、add、remove操作是安全的,多個(gè)線程可以安全地并發(fā)執(zhí)行插入、移除和訪問(wèn)操作。但是對(duì)于批量操作 addAll、removeAll、retainAll 和 containsAll并不能保證以原子方式執(zhí)行。理由很簡(jiǎn)單,因?yàn)閍ddAll、removeAll、retainAll底層調(diào)用的還是contains、add、remove的方法,在批量操作時(shí),只能保證每一次的contains、add、remove的操作是原子性的(即在進(jìn)行contains、add、remove三個(gè)操作時(shí),不會(huì)被其他線程打斷),而不能保證每一次批量的操作都不會(huì)被其他線程打斷。所以在進(jìn)行批量操作時(shí),需自行額外手動(dòng)做一些同步、加鎖措施,以此保證線程安全。另外,ConcurrentSkipListSet類(lèi)不允許使用 null 元素,因?yàn)闊o(wú)法可靠地將 null 參數(shù)及返回值與不存在的元素區(qū)分開(kāi)來(lái)。
4.HashMap對(duì)應(yīng)的ConcurrentHashMap
HashMap的并發(fā)安全版本是ConcurrentHashMap,但ConcurrentHashMap不允許 null 值。在大多數(shù)情況下,我們使用map都是讀取操作,寫(xiě)操作比較少。因此ConcurrentHashMap針對(duì)讀取操作做了大量的優(yōu)化,所以ConcurrentHashMap具有很高的并發(fā)性,在高并發(fā)場(chǎng)景下表現(xiàn)良好。關(guān)于ConcurrentHashMap詳細(xì)的內(nèi)容會(huì)在后續(xù)文章中進(jìn)行介紹。
5.TreeMap對(duì)應(yīng)的ConcurrentSkipListMap
ConcurrentSkipListMap的底層是通過(guò)跳表來(lái)實(shí)現(xiàn)的。跳表是一個(gè)鏈表,但是通過(guò)使用“跳躍式”查找的方式使得插入、讀取數(shù)據(jù)時(shí)復(fù)雜度變成了O(logn)。
有人曾比較過(guò)ConcurrentHashMap和ConcurrentSkipListMap的性能,在4線程1.6萬(wàn)數(shù)據(jù)的條件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有幾個(gè)ConcurrentHashMap不能比擬的優(yōu)點(diǎn):
- ConcurrentSkipListMap 的key是有序的
- ConcurrentSkipListMap 支持更高的并發(fā)。ConcurrentSkipListMap的存取時(shí)間是O(logn),和線程數(shù)幾乎無(wú)關(guān)。也就是說(shuō)在數(shù)據(jù)量一定的情況下,并發(fā)的線程越多,ConcurrentSkipListMap越能體現(xiàn)出其優(yōu)勢(shì)
在非多線程的情況下,應(yīng)當(dāng)盡量使用TreeMap。此外對(duì)于并發(fā)性相對(duì)較低的并行程序可以使Collections.synchronizedSortedMap將TreeMap進(jìn)行包裝,也可以提供較好的效率。對(duì)于高并發(fā)程序,應(yīng)當(dāng)使用ConcurrentSkipListMap,能夠提供更高的并發(fā)度。
所以在多線程程序中,如果需要對(duì)Map的鍵值進(jìn)行排序時(shí),請(qǐng)盡量使用ConcurrentSkipListMap,可能得到更好的并發(fā)度。
注意,調(diào)用ConcurrentSkipListMap的size時(shí),由于多個(gè)線程可以同時(shí)對(duì)映射表進(jìn)行操作,所以映射表需要遍歷整個(gè)鏈表才能返回元素個(gè)數(shù),這個(gè)操作是個(gè)O(log(n))的操作。