之前在看一些模擬面試的視頻時(shí),面試官問(wèn)到:“List如何保證線程安全“。
我腦海中首先想到的是使用List接口下的Vector集合。
然后面試者也同樣簡(jiǎn)單的說(shuō)出使用Vector集合。
但是面試官顯然對(duì)這個(gè)回答并不滿意。
那么List應(yīng)該如何保證線程安全呢?
這個(gè)問(wèn)題其實(shí)可以從《深入理解Java虛擬機(jī)》這本書中找到答案
絕對(duì)線程安全
絕對(duì)的線程安全能夠完全滿足Brian Goetz給出的線程安全的定義,這個(gè)定義其實(shí)是很嚴(yán)格的,一 個(gè)類要達(dá)到“不管運(yùn)行時(shí)環(huán)境如何,調(diào)用者都不需要任何額外的同步措施”可能需要付出非常高昂的, 甚至不切實(shí)際的代價(jià)。在Java API中標(biāo)注自己是線程安全的類,大多數(shù)都不是絕對(duì)的線程安全。我們 可以通過(guò)Java API中一個(gè)不是“絕對(duì)線程安全”的“線程安全類型”來(lái)看看這個(gè)語(yǔ)境里的“絕對(duì)”究竟是什么 意思。
如果說(shuō)java.util.Vector是一個(gè)線程安全的容器,相信所有的Java程序員對(duì)此都不會(huì)有異議,因?yàn)樗?的add()、get()和size()等方法都是被synchronized修飾的,盡管這樣效率不高,但保證了具備原子性、 可見(jiàn)性和有序性。不過(guò),即使它所有的方法都被修飾成synchronized,也不意味著調(diào)用它的時(shí)候就永遠(yuǎn) 都不再需要同步手段了,請(qǐng)看看代碼清單13-2中的測(cè)試代碼。
對(duì)Vector線程安全的測(cè)試
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println((vector.get(i)));
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20) ;
}
}
運(yùn)行結(jié)果如下:
Exception in thread "Thread-45907" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 17
at java.base/java.util.Vector.get(Vector.java:780)
at com.sleep.Demo01$2.run(Demo01.java:27)
at java.base/java.lang.Thread.run(Thread.java:834)
很明顯,盡管這里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多線程的環(huán)境 中,如果不在方法調(diào)用端做額外的同步措施,使用這段代碼仍然是不安全的。因?yàn)槿绻硪粋€(gè)線程恰 好在錯(cuò)誤的時(shí)間里刪除了一個(gè)元素,導(dǎo)致序號(hào)i已經(jīng)不再可用,再用i訪問(wèn)數(shù)組就會(huì)拋出一個(gè) ArrayIndexOutOfBoundsException異常。如果要保證這段代碼能正確執(zhí)行下去,我們不得不把 removeThread和printThread的定義改成代碼所示的這樣。
必須加入同步保證Vector訪問(wèn)的線程安全性
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
System.out.println((vector.get(i)));
}
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20) ;
}
}
假如Vector一定要做到絕對(duì)的線程安全,那就必須在它內(nèi)部維護(hù)一組一致性的快照訪問(wèn)才行,每次 對(duì)其中元素進(jìn)行改動(dòng)都要產(chǎn)生新的快照,這樣要付出的時(shí)間和空間成本都是非常大的。
相對(duì)線程安全
相對(duì)線程安全就是我們通常意義上所講的線程安全,它需要保證對(duì)這個(gè)對(duì)象單次的操作是線程安 全的,我們?cè)谡{(diào)用的時(shí)候不需要進(jìn)行額外的保障措施,但是對(duì)于一些特定順序的連續(xù)調(diào)用,就可能需 要在調(diào)用端使用額外的同步手段來(lái)保證調(diào)用的正確性。
CopyOnWriteArrayList
1.CopyOnWriteArrayList(字譯名稱:寫時(shí)復(fù)制),它可以看成是線程安全且讀操作無(wú)鎖的ArrayList。
2.使用場(chǎng)景:
讀操作遠(yuǎn)遠(yuǎn)大于寫操作,比如有些系統(tǒng)級(jí)別的信息,往往需要加載或者修改很少的次數(shù),但是會(huì)被系統(tǒng)內(nèi)的所有模塊頻繁的訪問(wèn)。
3.原理:
CopyOnWriteArrayList容器允許并發(fā)讀,讀操作時(shí)無(wú)鎖的,性能高。寫操作,比如向容器中添加一個(gè)元素,則首先將當(dāng)前容器復(fù)制一份,然后在新的副本上執(zhí)行寫操作(此時(shí)仍然可以讀取,讀取的時(shí)舊的容器中的數(shù)據(jù)),結(jié)束之后再將原容器的引用指向新容器。
特點(diǎn):這種鏈表,讀取完全不用加鎖,寫入也不會(huì)阻塞讀取,只有寫入和寫入之間需要進(jìn)行同步等待。
缺點(diǎn):1)占用內(nèi)存,每次執(zhí)行寫操作都要將原容器拷貝一份,數(shù)據(jù)量大時(shí),對(duì)內(nèi)存壓力較大,可能會(huì)引起頻繁GC
2)無(wú)法保證實(shí)時(shí)性,Vector對(duì)于讀寫操作都同步,保證了讀和寫的一致性,但是CopyOnWriteArrayList,寫和讀分別作用在新老不同的容器上,在寫的過(guò)程中,讀不會(huì)阻塞,但是讀取到的是老容器的數(shù)據(jù)。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
關(guān)于Collections.synchronizedList(List list)
CopyOnWriteArrayList和Collections.synchronizedList是實(shí)現(xiàn)線程安全的列表的兩種方式。兩種實(shí)現(xiàn)方式分別針對(duì)不同情況有不同的性能表現(xiàn),其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因?yàn)槭遣捎昧藄ynchronized關(guān)鍵字的方式,其讀操作性能并不如CopyOnWriteArrayList。因此在不同的應(yīng)用場(chǎng)景下,應(yīng)該選擇不同的多線程安全實(shí)現(xiàn)類。
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
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);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}