之前在看一些模擬面試的視頻時,面試官問到:“List如何保證線程安全“。
我腦海中首先想到的是使用List接口下的Vector集合。
然后面試者也同樣簡單的說出使用Vector集合。
但是面試官顯然對這個回答并不滿意。
那么List應(yīng)該如何保證線程安全呢?
這個問題其實可以從《深入理解Java虛擬機(jī)》這本書中找到答案
絕對線程安全
絕對的線程安全能夠完全滿足Brian Goetz給出的線程安全的定義,這個定義其實是很嚴(yán)格的,一 個類要達(dá)到“不管運行時環(huán)境如何,調(diào)用者都不需要任何額外的同步措施”可能需要付出非常高昂的, 甚至不切實際的代價。在Java API中標(biāo)注自己是線程安全的類,大多數(shù)都不是絕對的線程安全。我們 可以通過Java API中一個不是“絕對線程安全”的“線程安全類型”來看看這個語境里的“絕對”究竟是什么 意思。
如果說java.util.Vector是一個線程安全的容器,相信所有的Java程序員對此都不會有異議,因為它 的add()、get()和size()等方法都是被synchronized修飾的,盡管這樣效率不高,但保證了具備原子性、 可見性和有序性。不過,即使它所有的方法都被修飾成synchronized,也不意味著調(diào)用它的時候就永遠(yuǎn) 都不再需要同步手段了,請看看代碼清單13-2中的測試代碼。
對Vector線程安全的測試
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) ;
}
}
運行結(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)用端做額外的同步措施,使用這段代碼仍然是不安全的。因為如果另一個線程恰 好在錯誤的時間里刪除了一個元素,導(dǎo)致序號i已經(jīng)不再可用,再用i訪問數(shù)組就會拋出一個 ArrayIndexOutOfBoundsException異常。如果要保證這段代碼能正確執(zhí)行下去,我們不得不把 removeThread和printThread的定義改成代碼所示的這樣。
必須加入同步保證Vector訪問的線程安全性
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一定要做到絕對的線程安全,那就必須在它內(nèi)部維護(hù)一組一致性的快照訪問才行,每次 對其中元素進(jìn)行改動都要產(chǎn)生新的快照,這樣要付出的時間和空間成本都是非常大的。
相對線程安全
相對線程安全就是我們通常意義上所講的線程安全,它需要保證對這個對象單次的操作是線程安 全的,我們在調(diào)用的時候不需要進(jìn)行額外的保障措施,但是對于一些特定順序的連續(xù)調(diào)用,就可能需 要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性。
CopyOnWriteArrayList
1.CopyOnWriteArrayList(字譯名稱:寫時復(fù)制),它可以看成是線程安全且讀操作無鎖的ArrayList。
2.使用場景:
讀操作遠(yuǎn)遠(yuǎn)大于寫操作,比如有些系統(tǒng)級別的信息,往往需要加載或者修改很少的次數(shù),但是會被系統(tǒng)內(nèi)的所有模塊頻繁的訪問。
3.原理:
CopyOnWriteArrayList容器允許并發(fā)讀,讀操作時無鎖的,性能高。寫操作,比如向容器中添加一個元素,則首先將當(dāng)前容器復(fù)制一份,然后在新的副本上執(zhí)行寫操作(此時仍然可以讀取,讀取的時舊的容器中的數(shù)據(jù)),結(jié)束之后再將原容器的引用指向新容器。
特點:這種鏈表,讀取完全不用加鎖,寫入也不會阻塞讀取,只有寫入和寫入之間需要進(jìn)行同步等待。
缺點:1)占用內(nèi)存,每次執(zhí)行寫操作都要將原容器拷貝一份,數(shù)據(jù)量大時,對內(nèi)存壓力較大,可能會引起頻繁GC
2)無法保證實時性,Vector對于讀寫操作都同步,保證了讀和寫的一致性,但是CopyOnWriteArrayList,寫和讀分別作用在新老不同的容器上,在寫的過程中,讀不會阻塞,但是讀取到的是老容器的數(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是實現(xiàn)線程安全的列表的兩種方式。兩種實現(xiàn)方式分別針對不同情況有不同的性能表現(xiàn),其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關(guān)鍵字的方式,其讀操作性能并不如CopyOnWriteArrayList。因此在不同的應(yīng)用場景下,應(yīng)該選擇不同的多線程安全實現(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
}