Java并發(fā)編程之同步容器
簡(jiǎn)介
同步容器主要分兩類(lèi),一種是Vector這樣的普通類(lèi),一種是通過(guò)Collections的工廠方法創(chuàng)建的內(nèi)部類(lèi)
雖然很多人都對(duì)同步容器的性能低有偏見(jiàn),但它也不是一無(wú)是處,在這里我們插播一條阿里巴巴的開(kāi)發(fā)手冊(cè)規(guī)范:
高并發(fā)時(shí),同步調(diào)用應(yīng)該去考量鎖的性能損耗。能用無(wú)鎖數(shù)據(jù)結(jié)構(gòu),就不要用鎖;能鎖區(qū)塊,就不要鎖整個(gè)方法體;能用對(duì)象鎖,就不要用類(lèi)鎖。
可以看到,只有在高并發(fā)才會(huì)考慮到鎖的性能問(wèn)題,所以在一些小而全的系統(tǒng)中,同步容器還是有用武之地的(當(dāng)然也可以考慮并發(fā)容器,后面章節(jié)再討論)
一、什么是同步容器
定義:就是把容器類(lèi)同步化,這樣我們?cè)诓l(fā)中使用容器時(shí),就不用手動(dòng)同步,因?yàn)閮?nèi)部已經(jīng)自動(dòng)同步了
例子:比如Vector就是一個(gè)同步容器類(lèi),它的同步化就是把內(nèi)部的所有方法都上鎖(有的重載方法沒(méi)上鎖,但是最終調(diào)用的方法還是有鎖的)
源碼:Vector.add
// 通過(guò)synchronized為add方法上鎖
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
同步容器主要分兩類(lèi):
1.普通類(lèi):Vector、Stack、HashTable
2.內(nèi)部類(lèi):Collections創(chuàng)建的內(nèi)部類(lèi),比如Collections.SynchronizedList、 Collections.SynchronizedSet等
那這兩種有沒(méi)有區(qū)別呢?
當(dāng)然是有的,剛開(kāi)始的時(shí)候(Java1.0)只有第一種同步容器(Vector等)
但是因?yàn)閂ector這種類(lèi)太局氣了,它就想著把所有的東西都弄過(guò)來(lái)自己搞(Vector通過(guò)toArray轉(zhuǎn)為己有,HashTable通過(guò)putAll轉(zhuǎn)為己有);
源碼:Vector構(gòu)造函數(shù)
public Vector(Collection<? extends E> c) {
// 這里通過(guò)toArray將傳來(lái)的集合 轉(zhuǎn)為己有
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
所以就有了第二種同步容器類(lèi)(通過(guò)工廠方法創(chuàng)建的內(nèi)部容器類(lèi)),它就比較聰明了,它只是把原有的容器進(jìn)行包裝(通過(guò)this.list = list直接指向需要同步的容器),然后局部加鎖,這樣一來(lái),即生成了線程安全的類(lèi),又不用太費(fèi)力;
源碼:Collections.SynchronizedList構(gòu)造函數(shù)
SynchronizedList(List<E> list) {
super(list);
// 這里只是指向傳來(lái)的list,不轉(zhuǎn)為己有,后面的相關(guān)操作還是基于原有的list集合
this.list = list;
}
他們之間的區(qū)別如下:
| 兩種同步容器的區(qū)別 | 普通類(lèi) | 內(nèi)部類(lèi) |
| 鎖的對(duì)象 | 不可指定,只能this | 可指定,默認(rèn)this |
| 鎖的范圍 | 方法體(包括迭代) | 代碼塊(不包括迭代) |
| 適用范圍 | 窄-個(gè)別容器 | 廣-所有容器 |
這里我們重點(diǎn)說(shuō)下鎖的對(duì)象:
- 普通類(lèi)鎖的是當(dāng)前對(duì)象this(鎖在方法上,默認(rèn)this對(duì)象);
- 內(nèi)部類(lèi)鎖的是mutex屬性,這個(gè)屬性默認(rèn)是this,但是可以通過(guò)構(gòu)造函數(shù)(或工廠方法)來(lái)指定鎖的對(duì)象
源碼:Collections.SynchronizedCollection構(gòu)造函數(shù)
final Collection<E> c; // Backing Collection
// 這個(gè)就是鎖的對(duì)象
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
// 初始化為 this
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
這里要注意一點(diǎn)就是,內(nèi)部類(lèi)的迭代器沒(méi)有同步(Vector的迭代器有同步),需要手動(dòng)加鎖來(lái)同步
源碼:Vector.Itr.next 迭代方法(有上鎖)
public E next() {
synchronized (Vector.this) {
checkForComodification();
int i = cursor;
if (i >= elementCount)
throw new NoSuchElementException();
cursor = i + 1;
return elementData(lastRet = i);
}
}
源碼:Collections.SynchronizedCollection.iterator 迭代器(沒(méi)上鎖)
public Iterator<E> iterator() {
// 這里會(huì)直接實(shí)現(xiàn)類(lèi)的迭代器(比如ArrayList,它里面的迭代器肯定是沒(méi)上鎖的)
return c.iterator(); // Must be manually synched by user!
}
二、為什么要有同步容器
因?yàn)槠胀ǖ娜萜黝?lèi)(比如ArrayList)是線程不安全的,如果是在并發(fā)中使用,我們就需要手動(dòng)對(duì)其加鎖才會(huì)安全,這樣的話就很麻煩;
所以就有了同步容器,它來(lái)幫我們自動(dòng)加鎖
下面我們用代碼來(lái)對(duì)比下
線程不安全的類(lèi):ArrayList
public class SyncCollectionDemo {
private List<Integer> listNoSync;
public SyncCollectionDemo() {
this.listNoSync = new ArrayList<>();
}
public void addNoSync(int temp){
listNoSync.add(temp);
}
public static void main(String[] args) throws InterruptedException {
SyncCollectionDemo demo = new SyncCollectionDemo();
// 創(chuàng)建10個(gè)線程
for (int i = 0; i < 10; i++) {
// 每個(gè)線程執(zhí)行100次添加操作
new Thread(()->{
for (int j = 0; j < 1000; j++) {
demo.addNoSync(j);
}
}).start();
}
}
}
上面的代碼看似沒(méi)問(wèn)題,感覺(jué)就算有問(wèn)題也應(yīng)該是插入的順序比較亂(多線程交替插入)
但實(shí)際上運(yùn)行會(huì)發(fā)現(xiàn),可能會(huì)報(bào)錯(cuò)數(shù)組越界,如下所示:

原因有二:
因?yàn)锳rrayList.add操作沒(méi)有加鎖,導(dǎo)致多個(gè)線程可以同時(shí)執(zhí)行add操作add操作時(shí),如果發(fā)現(xiàn)list的容量不足,會(huì)進(jìn)行擴(kuò)容,但是由于多個(gè)線程同時(shí)擴(kuò)容,就會(huì)出現(xiàn)擴(kuò)容不足的問(wèn)題
源碼:ArrayList.grow擴(kuò)容
// 擴(kuò)容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 這里可以看到,每次擴(kuò)容增加一半的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到,擴(kuò)容是基于之前的容量進(jìn)行的,因此如果多個(gè)線程同時(shí)擴(kuò)容,那擴(kuò)容基數(shù)就不準(zhǔn)確了,結(jié)果就會(huì)有問(wèn)題
線程安全的類(lèi):Collections.SynchronizedList
/**
* <p>
* 同步容器類(lèi):為什么要有它
* </p>
*
* @author: JavaLover
* @time: 2021/5/3
*/
public class SyncCollectionDemo {
private List<Integer> listSync;
public SyncCollectionDemo() {
// 這里包裝一個(gè)空的ArrayList
this.listSync = Collections.synchronizedList(new ArrayList<>());
}
public void addSync(int j){
// 內(nèi)部是同步操作: synchronized (mutex) {return c.add(e);}
listSync.add(j);
}
public static void main(String[] args) throws InterruptedException {
SyncCollectionDemo demo = new SyncCollectionDemo();
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
demo.addSync(j);
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
// 輸出1000
System.out.println(demo.listSync.size());
}
}
輸出正確,因?yàn)楝F(xiàn)在ArrayList被Collections包裝成了一個(gè)線程安全的類(lèi)
這就是為啥會(huì)有同步容器的原因:因?yàn)橥饺萜魇沟貌l(fā)編程時(shí),線程更加安全
三、同步容器的優(yōu)缺點(diǎn)
一般來(lái)說(shuō),都是先說(shuō)優(yōu)點(diǎn),再說(shuō)缺點(diǎn)
但是我們這次先說(shuō)優(yōu)點(diǎn)
優(yōu)點(diǎn):
- 并發(fā)編程中,獨(dú)立操作是線程安全的,比如單獨(dú)的add操作
缺點(diǎn)(是的,優(yōu)點(diǎn)已經(jīng)說(shuō)完了):
- 性能差,基本上所有方法都上鎖,完美的詮釋了“寧可錯(cuò)殺一千,不可放過(guò)一個(gè)”
- 復(fù)合操作,還是不安全,比如putIfAbsent操作(如果沒(méi)有則添加)
- 快速失敗機(jī)制,這種機(jī)制會(huì)報(bào)錯(cuò)提示
ConcurrentModificationException,一般出現(xiàn)在當(dāng)某個(gè)線程在遍歷容器時(shí),其他線程恰好修改了這個(gè)容器的長(zhǎng)度
為啥第三點(diǎn)是缺點(diǎn)呢?
因?yàn)樗荒茏鳛橐粋€(gè)建議,告訴我們有并發(fā)修改異常,但是不能保證每個(gè)并發(fā)修改都會(huì)爆出這個(gè)異常
爆出這個(gè)異常的前提如下:
源碼:Vector.Itr.checkForComodification 檢查容器修改次數(shù)
final void checkForComodification() {
// modCount:容器的長(zhǎng)度變化次數(shù), expectedModCount:期望的容器的長(zhǎng)度變化次數(shù)
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那什么情況下并發(fā)修改不會(huì)爆出異常呢?有兩種:
1.遍歷沒(méi)加鎖的情況:對(duì)于第二種同步容器(Collections內(nèi)部類(lèi))來(lái)說(shuō),假設(shè)線程A修改了modCount的值,但是沒(méi)有同步到線程B,那么線程B遍歷就不會(huì)發(fā)生異常(但實(shí)際上問(wèn)題已經(jīng)存在了,只是暫時(shí)沒(méi)有出現(xiàn))
2.依賴(lài)線程執(zhí)行順序的情況:對(duì)于所有的同步容器來(lái)說(shuō),假設(shè)線程B已經(jīng)遍歷完了容器,此時(shí)線程A才開(kāi)始遍歷修改,那么也不會(huì)發(fā)生異常
代碼就不貼了,大家感興趣的可以直接寫(xiě)幾個(gè)線程遍歷試試,多運(yùn)行幾次,應(yīng)該就可以看到效果(不過(guò)第一種情況也是基于理論分析,實(shí)際代碼我這邊也沒(méi)跑出來(lái))
根據(jù)阿里巴巴的開(kāi)發(fā)規(guī)范:不要在 foreach 循環(huán)里進(jìn)行元素的 remove/add 操作。remove 元素請(qǐng)使用 Iterator方式,如果并發(fā)操作,需要對(duì) Iterator 對(duì)象加鎖。
這里解釋下,關(guān)于List.remove和Iterator.remove的區(qū)別
- Iterator.remove:會(huì)同步修改expectedModCount=modCount
- list.remove:只會(huì)修改modCount,因?yàn)閑xpectedModCount屬于iterator對(duì)象的屬性,不屬于list的屬性(但是也可以間接訪問(wèn))
源碼:ArrayList.remove移除元素操作
public E remove(int index) {
rangeCheck(index);
// 1. 這里修改了 modCount
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
源碼:ArrayList.Itr.remove迭代器移除元素操作
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 1. 這里調(diào)用上面介紹的list.romove,修改modCount
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 2. 這里再同步更新 expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
由于同步容器的這些缺點(diǎn),于是就有了并發(fā)容器(下期來(lái)介紹)
四、同步容器的使用場(chǎng)景
多用在并發(fā)編程,但是并發(fā)量又不是很大的場(chǎng)景,比如一些簡(jiǎn)單的個(gè)人博客系統(tǒng)(具體多少并發(fā)量算大,這個(gè)也是分很多情況而論的,并不是說(shuō)每秒處理超過(guò)多少個(gè)請(qǐng)求,就說(shuō)是高并發(fā),還要結(jié)合吞吐量、系統(tǒng)響應(yīng)時(shí)間等多個(gè)因素一起考慮)
具體點(diǎn)來(lái)說(shuō)的話,有以下幾個(gè)場(chǎng)景:
- 寫(xiě)多讀少,這個(gè)時(shí)候同步容器和并發(fā)容器的性能差別不大(并發(fā)容器可以并發(fā)讀)
- 自定義的復(fù)合操作,比如getLast等操作(putIfAbsent就算了,因?yàn)椴l(fā)容器有默認(rèn)提供這個(gè)復(fù)合操作)
- 等等
總結(jié)
什么是同步容器:就是把容器類(lèi)同步化,這樣我們?cè)诓l(fā)中使用容器時(shí),就不用手動(dòng)同步,因?yàn)閮?nèi)部已經(jīng)自動(dòng)同步了
為什么要有同步容器:因?yàn)槠胀ǖ娜萜黝?lèi)(比如ArrayList)是線程不安全的,如果是在并發(fā)中使用,我們就需要手動(dòng)對(duì)其加鎖才會(huì)安全,這樣的話就很太麻煩;所以就有了同步容器,它來(lái)幫我們自動(dòng)加鎖
同步容器的優(yōu)缺點(diǎn):
缺點(diǎn) 復(fù)合操作,還是不安全,性能差快速失敗機(jī)制,只適合bug調(diào)試
同步容器的使用場(chǎng)景
多用在并發(fā)量不是很大的場(chǎng)景,比如個(gè)人博客、后臺(tái)系統(tǒng)等
具體點(diǎn)來(lái)說(shuō),有以下幾個(gè)場(chǎng)景:
- 寫(xiě)多讀少:這個(gè)時(shí)候同步容器和并發(fā)容器差別不是很大
- 自定義復(fù)合操作:比如getLast等復(fù)合操作,因?yàn)橥饺萜鞫际菃蝹€(gè)操作進(jìn)行上鎖的,所以可以很方便地去拼接復(fù)合操作(記得外部加鎖)
到此這篇關(guān)于Java并發(fā)編程之同步容器的文章就介紹到這了,更多相關(guān)Java同步容器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java8 如何實(shí)現(xiàn)分組計(jì)算數(shù)量和計(jì)算總數(shù)
這篇文章主要介紹了java8 如何實(shí)現(xiàn)分組計(jì)算數(shù)量和計(jì)算總數(shù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
JavaSE實(shí)現(xiàn)圖書(shū)管理系統(tǒng)的示例代碼
這篇博客是在學(xué)習(xí)了一部分Java基礎(chǔ)語(yǔ)法之后的練習(xí)項(xiàng)目,通過(guò)這個(gè)小項(xiàng)目的練習(xí),對(duì)Java中的類(lèi)和對(duì)象,抽象類(lèi)和接口等進(jìn)行熟悉理解??旄S小編一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
spring boot整合quartz實(shí)現(xiàn)多個(gè)定時(shí)任務(wù)的方法
這篇文章主要介紹了spring boot整合quartz實(shí)現(xiàn)多個(gè)定時(shí)任務(wù)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
Java 反射調(diào)用靜態(tài)方法的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇Java 反射調(diào)用靜態(tài)方法的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06
Java+MySql圖片數(shù)據(jù)保存與讀取的具體實(shí)例
之前一直沒(méi)有做過(guò)涉及到圖片存儲(chǔ)的應(yīng)用,最近要做的東東涉及到了這個(gè)點(diǎn),就做了一個(gè)小的例子算是對(duì)圖片存儲(chǔ)的初試吧2013-06-06
Spring 源碼解析CommonAnnotationBeanPostProcessor
這篇文章主要為大家介紹了Spring 源碼解析CommonAnnotationBeanPostProcessor示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Java中的List和MySQL中的varchar相互轉(zhuǎn)換的解決方案
實(shí)體類(lèi)中有一個(gè) List<String> 類(lèi)型的屬性,對(duì)應(yīng)于 MySQL 表里的 varchar 字段,使用 MyBatis 添加或查詢(xún)時(shí)能互相轉(zhuǎn)換,本文給大家介紹Java中的List和MySQL中的varchar相互轉(zhuǎn)換的解決方案,需要的朋友可以參考下2024-06-06
Java System類(lèi)兩個(gè)常用方法代碼實(shí)例
這篇文章主要介紹了Java System類(lèi)兩個(gè)常用方法代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02

