讀多寫少的場景下引起的問題?數組
假設如今咱們的內存裏有一個 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 變量來賦值保證可見性,更新的時候對讀線程沒有任何的影響!