copy-on-write簡稱cow,是一種用于程式設計中的優化政策。其基本思路是,從一開始大家都在共享同一個内容,當某個人想要修改這個内容的時候,才會真正把内容copy出去形成一個新的内容然後再改,這是一種延時懶惰政策。從jdk1.5開始java并發包裡提供了兩個使用copyonwrite機制實作的并發容器,它們是copyonwritearraylist和copyonwritearrayset。copyonwrite容器非常有用,可以在非常多的并發場景中使用到。
copyonwrite容器即寫時複制的容器。通俗的了解是當我們往一個容器添加元素的時候,不直接往目前容器添加,而是先将目前容器進行copy,複制出一個新的容器,然後新的容器裡添加元素,添加完元素之後,再将原容器的引用指向新的容器。這樣做的好處是我們可以對copyonwrite容器進行并發的讀,而不需要加鎖,因為目前容器不會添加任何元素。是以copyonwrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
在使用copyonwritearraylist之前,我們先閱讀其源碼了解下它是如何實作的。以下代碼是向arraylist裡添加元素,可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會copy出n個副本出來。
<code>01</code>
<code>public</code> <code>boolean</code> <code>add(t e) {</code>
<code>02</code>
<code> </code><code>final</code> <code>reentrantlock lock = </code><code>this</code><code>.lock;</code>
<code>03</code>
<code> </code><code>lock.lock();</code>
<code>04</code>
<code> </code><code>try</code> <code>{</code>
<code>05</code>
<code>06</code>
<code> </code><code>object[] elements = getarray();</code>
<code>07</code>
<code>08</code>
<code> </code><code>int</code> <code>len = elements.length;</code>
<code>09</code>
<code> </code><code>// 複制出新數組</code>
<code>10</code>
<code>11</code>
<code> </code><code>object[] newelements = arrays.copyof(elements, len + </code><code>1</code><code>);</code>
<code>12</code>
<code> </code><code>// 把新元素添加到新數組裡</code>
<code>13</code>
<code>14</code>
<code> </code><code>newelements[len] = e;</code>
<code>15</code>
<code> </code><code>// 把原數組引用指向新數組</code>
<code>16</code>
<code>17</code>
<code> </code><code>setarray(newelements);</code>
<code>18</code>
<code>19</code>
<code> </code><code>return</code> <code>true</code><code>;</code>
<code>20</code>
<code>21</code>
<code> </code><code>} </code><code>finally</code> <code>{</code>
<code>22</code>
<code>23</code>
<code> </code><code>lock.unlock();</code>
<code>24</code>
<code>25</code>
<code> </code><code>}</code>
<code>26</code>
<code>27</code>
<code>}</code>
<code>28</code>
<code>29</code>
<code>final</code> <code>void</code> <code>setarray(object[] a) {</code>
<code>30</code>
<code> </code><code>array = a;</code>
<code>31</code>
讀的時候不需要加鎖,如果讀的時候有多個線程正在向arraylist添加資料,讀還是會讀到舊的資料,因為寫的時候不會鎖住舊的arraylist。
<code>1</code>
<code>public</code> <code>e get(</code><code>int</code> <code>index) {</code>
<code>2</code>
<code> </code><code>return</code> <code>get(getarray(), index);</code>
<code>3</code>
jdk中并沒有提供copyonwritemap,我們可以參考copyonwritearraylist來實作一個,基本代碼如下:
<code>import</code> <code>java.util.collection;</code>
<code>import</code> <code>java.util.map;</code>
<code>import</code> <code>java.util.set;</code>
<code>public</code> <code>class</code> <code>copyonwritemap<k, v> </code><code>implements</code> <code>map<k, v>, cloneable {</code>
<code> </code><code>private</code> <code>volatile</code> <code>map<k, v> internalmap;</code>
<code> </code><code>public</code> <code>copyonwritemap() {</code>
<code> </code><code>internalmap = </code><code>new</code> <code>hashmap<k, v>();</code>
<code> </code><code>public</code> <code>v put(k key, v value) {</code>
<code> </code><code>synchronized</code> <code>(</code><code>this</code><code>) {</code>
<code> </code><code>map<k, v> newmap = </code><code>new</code> <code>hashmap<k, v>(internalmap);</code>
<code> </code><code>v val = newmap.put(key, value);</code>
<code> </code><code>internalmap = newmap;</code>
<code> </code><code>return</code> <code>val;</code>
<code> </code><code>}</code>
<code> </code><code>public</code> <code>v get(object key) {</code>
<code> </code><code>return</code> <code>internalmap.get(key);</code>
<code> </code><code>public</code> <code>void</code> <code>putall(map<? </code><code>extends</code> <code>k, ? </code><code>extends</code> <code>v> newdata) {</code>
<code> </code><code>newmap.putall(newdata);</code>
<code>32</code>
<code>33</code>
實作很簡單,隻要了解了copyonwrite機制,我們可以實作各種copyonwrite容器,并且在不同的應用場景中使用。
copyonwrite并發容器用于讀多寫少的并發場景。比如白名單,黑名單,商品類目的通路和更新場景,假如我們有一個搜尋網站,使用者在這個網站的搜尋框中,輸入關鍵字搜尋内容,但是某些關鍵字不允許被搜尋。這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當使用者搜尋時,會檢查目前關鍵字在不在黑名單當中,如果在,則提示不能搜尋。實作代碼如下:
<code>package</code> <code>com.ifeve.book;</code>
<code>import</code> <code>com.ifeve.book.forkjoin.copyonwritemap;</code>
<code>/**</code>
<code> </code><code>* 黑名單服務</code>
<code> </code><code>*</code>
<code> </code><code>* @author fangtengfei</code>
<code> </code><code>*/</code>
<code>public</code> <code>class</code> <code>blacklistserviceimpl {</code>
<code> </code><code>private</code> <code>static</code> <code>copyonwritemap<string, boolean> blacklistmap = </code><code>new</code><code>copyonwritemap<string, boolean>(</code>
<code> </code><code>1000</code><code>);</code>
<code> </code><code>public</code> <code>static</code> <code>boolean</code> <code>isblacklist(string id) {</code>
<code> </code><code>return</code> <code>blacklistmap.get(id) == </code><code>null</code> <code>? </code><code>false</code> <code>: </code><code>true</code><code>;</code>
<code> </code><code>public</code> <code>static</code> <code>void</code> <code>addblacklist(string id) {</code>
<code> </code><code>blacklistmap.put(id, boolean.true);</code>
<code> </code><code>/**</code>
<code> </code><code>* 批量添加黑名單</code>
<code> </code><code>*</code>
<code> </code><code>* @param ids</code>
<code> </code><code>*/</code>
<code> </code><code>public</code> <code>static</code> <code>void</code> <code>addblacklist(map<string,boolean> ids) {</code>
<code> </code><code>blacklistmap.putall(ids);</code>
<code>34</code>
<code>35</code>
代碼很簡單,但是使用copyonwritemap需要注意兩件事情:
1. 減少擴容開銷。根據實際需要,初始化copyonwritemap的大小,避免寫時copyonwritemap擴容的開銷。
2. 使用批量添加。因為每次添加,容器每次都會進行複制,是以減少添加次數,可以減少容器的複制次數。如使用上面代碼裡的addblacklist方法。
copyonwrite容器有很多優點,但是同時也存在兩個問題,即記憶體占用問題和資料一緻性問題。是以在開發的時候需要注意一下。
記憶體占用問題。因為copyonwrite的寫時複制機制,是以在進行寫操作的時候,記憶體裡會同時駐紮兩個對象的記憶體,舊的對象和新寫入的對象(注意:在複制的時候隻是複制容器裡的引用,隻是在寫的時候會建立新對象添加到新容器裡,而舊容器的對象還在使用,是以有兩份對象記憶體)。如果這些對象占用的記憶體比較大,比如說200m左右,那麼再寫入100m資料進去,記憶體就會占用300m,那麼這個時候很有可能造成頻繁的yong gc和full gc。之前我們系統中使用了一個服務由于每晚使用copyonwrite機制更新大對象,造成了每晚15秒的full gc,應用響應時間也随之變長。
資料一緻性問題。copyonwrite容器隻能保證資料的最終一緻性,不能保證資料的實時一緻性。是以如果你希望寫入的的資料,馬上能讀到,請不要使用copyonwrite容器。