ArrayList 坑 的引發思考

單線程 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

/**

 * Created by zejian on 2017/6/4.

 * Blog : http://blog.csdn.net/javazejian 

 * 消除StringBuffer同步鎖

 */

public class StringBufferRemoveSync {

 

    public void add(String str1, String str2) {

        //StringBuffer是線程安全,由於sb只會在append方法中使用,不可能被其他線程引用

        //因此sb屬於不可能共享的資源,JVM會自動消除內部的鎖

        StringBuffer sb = new StringBuffer();

        sb.append(str1).append(str2);

    }

 

    public static void main(String[] args) {

        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();

        for (int i = 0; i < 10000000; i++) {

            rmsync.add("abc""123");

        }

    }

 

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章