天天看點

聊聊并發-Java中的Copy-On-Write容器

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&lt;k, v&gt; </code><code>implements</code> <code>map&lt;k, v&gt;, cloneable {</code>

<code>    </code><code>private</code> <code>volatile</code> <code>map&lt;k, v&gt; internalmap;</code>

<code>    </code><code>public</code> <code>copyonwritemap() {</code>

<code>        </code><code>internalmap = </code><code>new</code> <code>hashmap&lt;k, v&gt;();</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&lt;k, v&gt; newmap = </code><code>new</code> <code>hashmap&lt;k, v&gt;(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&lt;? </code><code>extends</code> <code>k, ? </code><code>extends</code> <code>v&gt; 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&lt;string, boolean&gt; blacklistmap = </code><code>new</code><code>copyonwritemap&lt;string, boolean&gt;(</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&lt;string,boolean&gt; 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容器。