單線程 ArrayList.remove()的坑
public static void main(String[] args) {
singleThread();
}
public static void singleThread(){
ArrayList<String> list = new ArrayList<String>();
list.add("劉一");
list.add("劉二");
list.add("單點");
list.add("等待");
list.add("餓餓");
Iterator iter = list.iterator();
while(iter.hasNext()){
String str = (String) iter.next();
if(str.equals("單點")){
list.remove(str);
}
}
System.out.println(list.size());
}
上面這段問題代碼引發的思考 ,運行報下面異常
org.xxy.rpc.controller.ArrayListDemo
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at org.xxy.rpc.controller.ArrayListDemo.singleThread(ArrayListDemo.java:21)
at org.xxy.rpc.controller.ArrayListDemo.main(ArrayListDemo.java:9)
Process finished with exit code 1
通過錯誤提示;查看源碼ArrayList.java:859 行;ArrayList 內部類Itr實現的迭代器 next()方法:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
next方法一上來 就調了checkForComodification 方法;
再 checkForComodification方法裏 modCount != expectedModCount 就報異常;如下代碼
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
問題找了到了modCount != expectedModCount導致;
這倆是SM東西??? 下面介紹他倆
下面是Arraylist.remove(object o)方法源碼
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
沒有發現什麼問題,繼續看 fastRemove(index)
fastRemove(int index)刪除時,將 modCount++了 ;
expectedModCount值沒有發現身影,
那豈不是 迭代器再next就報異常了;
問題好像清晰了;
再一看 刪除操作是通過數組 copy 實現的,果然還是數組;
數組 copy是 原生方法哦
private void fastRemove(int index) {
modCount++;
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
}
結論:
1.modCount 是ArrayList 抽象父類AbstractList 所有,記錄 結構上修改此列表的次數。
2.expectedModCount 是ArrayList 內部類 Itr implements Iterator 私有的;
1.ArrayList 的 Iterator.next()方法會校驗 expectedModCount == modCount,不一致就報 ConcurrentModificationException; 2.ArrayList.remove(object o)方法 會修改 ArrayList繼承來的modCount;不會修改內部類 Itr裏的 expectedModCount ;3.ArrayList 內部類 Itr遍歷時。修改請使用迭代器自帶的方法 如Iterator.remove();
在ArrayList 迭代器方法裏;發現了內部類 Itr
public Iterator<E> iterator() {
return new Itr();
}
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; //倆個值一致
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
Iterator.remove()方法源碼,原因傳參不爲null ; 我們留意下下面else 裏
也是先校驗修改次數,這個在多線程裏也是會報錯的;下面會再寫一文說明
在單線程裏沒有問題, 校驗完成後 調用 ArrayList.remove(object o) ;刪除裏 在修改內部 expectedModCount = modCount;
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
多線程 ArrayList 內部 Iterator.remove() 的坑
上面說了 ArrayList 再多線程裏Iterator.remove()也有問題 請看下面示例
直接運行也報異常 ConcurrentModificationException;想必原因大家都想到了;
Iterator.remove() 沒有加鎖,多線程併發時,導致 modCount值 髒讀;不安全;
private static void multiThread(){
final ArrayList<String> list = new ArrayList<String>();
list.add("劉一");
list.add("劉二");
list.add("單點");
list.add("等待");
list.add("餓餓");
new Thread(new Runnable() {
public void run() {
Iterator iter = list.iterator();
while (iter.hasNext()) {
String str = (String) iter.next();
if (str.equals("單點")) {
iter.remove();
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
Iterator iter = list.iterator();
while (iter.hasNext()) {
String str = (String) iter.next();
if (str.equals("劉二")) {
iter.remove();
}
System.out.println(str);
}
System.out.println(list.size());
}
}).start();
}
錯誤
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at org.xxy.rpc.controller.ArrayListDemo$2.run(ArrayListDemo.java:58)
at java.lang.Thread.run(Thread.java:748)
問題來了 ArrayList 也沒有多線程安全的呢???有 CopyOnWriteArrayList 這個是多線程安全的;
爲什麼CopyOnWriteArrayList 是安全的;我們分析下
CopyOnWriteArrayList 的源碼學習
CopyOnWriteArrayList的最開始我是再數據驅動註冊源碼裏看到的;
public class DriverManager {
// 註冊了JDBC驅動的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new
CopyOnWriteArrayList<>();
......省略
}
CopyOnWriteArrayList 的多線程安全是通過 ReentrantLock 鎖實現的;我們看下CopyOnWriteArrayList 的新增元素方法的實現add(E e)
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();
}
}
我們看到它再操作時;首先聲明瞭一把 ReentrantLock 鎖,再lock ,最後結束時 unlock;
ReentrantLock鎖我們下面會單獨介紹;
現在我們看看add 方法除了鎖之外,其他的東西;首先獲取當前數據對象 elements = getArray();
再 複製了一份數組對象 放在 新的數組 newElements裏,新數組裏進行了擴容+1 操作;也就是說新數組比舊數據長度多1;
最後一位就是多出來的,放了這個增加元素;
然後呢》》將新數組 賦給了CopyOnWriteArrayList的存儲數組 array;
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
現在我們再看看CopyOnWriteArrayList的讀源碼
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
它的讀就簡單的多了;沒有什麼花操作,直接返回數組值;也沒有加鎖;這裏有個問題,就是寫和讀同時發生時,
因爲寫操作分好幾個步驟(copt舊數據,增加新節點元素,修改舊數組指向)會有讀到的是舊數組問題;
volatile 修飾 array 是爲了禁止指令重排,和 內存可見性(工作內存與主存一致性);這裏看看 JMM ,
特別說明 volatile 修飾下也 不是原則性的;
通過上面CopyOnWriteArrayList的源碼解讀我們發現他的特點:
1.寫時複製機制
2.寫操作加鎖|解鎖
3.讀操作不加鎖,數據
4.體現讀寫分離和最終一致性;
上面說了CopyOnWriteArrayList的多線程是通過ReenTranLock 重複鎖實現的;下面我們來說下
鎖;java裏synchronized 和ReenTranLock倆類鎖;他們有什麼區別 值得思考;
Synchronized與ReentrantLock區別
Synchronized是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成
Synchronized是依賴於JVM實現的,而ReenTrantLock是JDK實現的,有什麼區別,說白了就類似於操作系統來控制實現和用戶自己敲代碼實現的區別。前者的實現是比較難見到的,後者有直接的源碼可供閱讀。
很明顯Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來加鎖和釋放鎖,爲了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。
鎖的細粒度和靈活度:很明顯ReenTrantLock優於Synchronized
Synchronized優化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑑了ReenTrantLock中的CAS技術【內存值,舊值,期望值】。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
相比Synchronized,ReentrantLock類提供了一些高級功能,主要有以下3項:
1.等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。通過lock.lockInterruptibly()來實現這個機制。
2.公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是創建的非公平鎖,可以通過參數true設爲公平鎖,但公平鎖表現的性能不是很好。
公平鎖、非公平鎖的創建方式:
//創建一個非公平鎖,默認是非公平鎖
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
//創建一個公平鎖,構造傳參true
Lock lock = new ReentrantLock(true);
3.鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象。ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的線程們,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒全部線程。
什麼情況下使用ReenTrantLock:
答案是,如果你需要實現ReenTrantLock的三個獨有功能時。
class MyThread implements Runnable {
private Lock lock=new ReentrantLock();
public void run() {
lock.lock();
try{
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+":"+i);
}finally{
lock.unlock();
}
}
Java虛擬機對synchronize的優化:
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,關於重量級鎖,前面我們已詳細分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優化手段。
偏向鎖
偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此爲了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因爲這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹爲重量級鎖,而是先升級爲輕量級鎖。
輕量級鎖
倘若偏向鎖失敗,虛擬機並不會立即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹爲重量級鎖。
自旋鎖
輕量級鎖失敗後,虛擬機爲了避免線程真實地在操作系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱爲自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級爲重量級鎖了。
鎖消除
消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個局部變量,並且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|