CopyOnWrite 思想及在 Java 併發包中的具體體現

讀多寫少的場景下引起的問題?數組

假設如今咱們的內存裏有一個 ArrayList,這個 ArrayList 默認狀況下確定是線程不安全的,要是多個線程併發讀和寫這個 ArrayList 可能會有問題。安全

那麼,問題來了,咱們應該怎麼讓這個 ArrayList 變成線程安全的呢?多線程

有一個很是簡單的辦法,對這個 ArrayList 的訪問都加上線程同步的控制,好比說必定要在 Synchronized 代碼段來對這個 ArrayList 進行訪問,這樣的話,就能同一時間就讓一個線程來操做它了,或者是用 ReadWriteLock 讀寫鎖的方式來控制,均可以。併發

咱們假設就是用 ReadWriteLock 讀寫鎖的方式來控制對這個 ArrayList 的訪問,這樣多個讀請求能夠同時執行從 ArrayList 裏讀取數據,可是讀請求和寫請求之間互斥,寫請求和寫請求也是互斥的。性能

代碼大概就是相似下面這樣:this

public Object  read() { 
    lock.readLock().lock(); 
    // 對ArrayList讀取 
    lock.readLock().unlock(); 
} 
public void write() { 
    lock.writeLock().lock(); 
    // 對ArrayList寫 
    lock.writeLock().unlock(); 
} 

相似上面的代碼有什麼問題呢?spa

最大的問題,其實就在於寫鎖和讀鎖的互斥。假設寫操做頻率很低,讀操做頻率很高,是寫少讀多的場景。那麼偶爾執行一個寫操做的時候,是否是會加上寫鎖,此時大量的讀操做過來是否是就會被阻塞住,沒法執行?這個就是讀寫鎖可能遇到的最大的問題。線程

引入 CopyOnWrite 思想解決問題翻譯

這個時候就要引入 CopyOnWrite 思想來解決問題了。它的思想就是,不用加什麼讀寫鎖,把鎖通通去掉,有鎖就有問題,有鎖就有互斥,有鎖就可能致使性能低下,會阻塞請求,致使別的請求都卡着不能執行。code

那麼它怎麼保證多線程併發的安全性呢?

很簡單,顧名思義,利用「CopyOnWrite」的方式,這個英語翻譯成中文,大概就是「寫數據的時候利用拷貝的副原本執行」。你在讀數據的時候,其實不加鎖也不要緊,你們左右都是一個讀罷了,互相沒影響。問題主要是在寫的時候,寫的時候你既然不能加鎖了,那麼就得采用一個策略。假如說你的 ArrayList 底層是一個數組來存放你的列表數據,那麼這時好比你要修改這個數組裏的數據,你就必須先拷貝這個數組的一個副本。而後你能夠在這個數組的副本里寫入你要修改的數據,可是在這個過程當中實際上你都是在操做一個副本而已。

這樣的話,讀操做是否是能夠同時正常的執行?這個寫操做對讀操做是沒有任何的影響的吧!

看下面的圖,來體會一下這個過程:

     

關鍵問題來了,那那個寫線程如今把副本數組給修改完了,如今怎麼才能讓讀線程感知到這個變化呢?

這裏要配合上 Volatile 關鍵字的使用, Volatile 關鍵字的核心就是讓一個變量被寫線程給修改以後,立馬讓其餘線程能夠讀到這個變量引用的最近的值,這就是 Volatile 最核心的做用。

因此一旦寫線程搞定了副本數組的修改以後,那麼就能夠用 Volatile 寫的方式,把這個副本數組賦值給 Volatile 修飾的那個數組的引用變量了。只要一賦值給那個 Volatile 修飾的變量,立馬就會對讀線程可見,你們都能看到最新的數組了。

下面是 JDK 裏的 CopyOnWriteArrayList 的源碼:

// 這個數組是核心的,由於用volatile修飾了 
// 只要把最新的數組對他賦值,其餘線程立馬能夠看到最新的數組 
private transient volatile Object[] array; 

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; 
       // 而後把副本數組賦值給volatile修飾的變量 
       setArray(newElements); 
       return true; 
   } finally { 
       lock.unlock(); 
   } 
} 

咱們能夠看看寫數據的時候,它是怎麼拷貝一個數組副本,而後修改副本,接着經過 Volatile 變量賦值的方式,把修改好的數組副本給更新回去,立馬讓其餘線程可見的。

由於是經過副原本進行更新的,萬一要是多個線程都要同時更新呢?那搞出來多個副本會不會有問題?

固然不能多個線程同時更新了,這個時候就是看上面源碼裏,加入了 Lock 鎖的機制,也就是同一時間只有一個線程能夠更新。

那麼更新的時候,會對讀操做有任何的影響嗎?

絕對不會,由於讀操做就是很是簡單的對那個數組進行讀而已,不涉及任何的鎖。並且只要他更新完畢對 Volatile 修飾的變量賦值,那麼讀線程立馬能夠看到最新修改後的數組,這是 Volatile 保證的:

private E get(Object[] a, int index) { 
    // 最簡單的對數組進行讀取 
    return (E) a[index]; 
} 

這樣就完美解決了咱們以前說的讀多寫少的問題。若是用讀寫鎖互斥的話,會致使寫鎖阻塞大量讀操做,影響併發性能。可是若是用了 CopyOnWriteArrayList,就是用空間換時間,更新的時候基於副本更新,避免鎖,而後最後用 Volatile 變量來賦值保證可見性,更新的時候對讀線程沒有任何的影響!

相關文章
相關標籤/搜索