話をしながら、(9)JavaのCopy-On-Write容器について話します。

4347 ワード

Copy-On-Write略称COWは、プログラム設計における最適化戦略である。その基本的な考え方は、最初からみんなが同じ内容を共有しています。ある人がこの内容を直したいと思ったら、本当にCopyを外に出して新しい内容を形成してから変えます。これは遅延怠け者の策略です。JDK 1.5からJavaを開始し、パッケージにはCopyOWrite機構を用いて実現される2つの同時容器が提供されています。これらはCopyOnWriteArayListとCopyOnWriteAraySetです。CopyOWrite容器は非常に有用で、多くの同時シーンで使用できます。
CopyOWrite容器とは?
CopyOWrite容器は書き込み時にコピーした容器です。分かりやすいのは、私達が一つの容器に元素を添加する時、直接に現在の容器に添加しないで、先に現在の容器をCopyして、新しい容器を複製して、新しい容器に元素を追加して、元素を追加した後、元の容器の引用を新しい容器に指します。このようにするメリットは、CopyOWrite容器を同時に読むことができます。ロックをかける必要はありません。現在の容器には何の元素も添加されていません。だからCopyOWrite容器も読み书きと书き分けの思想で、読み书きと书き方が违う容器です。
CopyOWriteArayListの実現原理
CopyOWriteArayListを使う前に、まずそのソースコードを読んで、どうやって実現されたのかを確認します。以下のコードはArayListに元素を添加するもので、添加する時はロックをかける必要があります。そうでないとマルチスレッドで書く時はCopyがN個のコピーを出します。
public boolean add(T 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();
    }
}

final void setArray(Object[] a) {
    array = a;
}
読む時はロックは必要ありません。読む時に複数のスレッドがアラーリストにデータを追加しています。読む時は古いデータを読みます。書く時は古いアラーリストをロックしません。
public E get(int index) {
    return get(getArray(), index);
}
JDKではCopyOWriteMapは提供されていません。CopyOnWriteArayListを参照して実現できます。基本コードは以下の通りです。
import java.util.Collection;
import java.util.Map;
import java.util.Set;

public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
    private volatile Map<K, V> internalMap;

    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }

    public V put(K key, V value) {

        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }

    public V get(Object key) {
        return internalMap.get(key);
    }

    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }
}
実現はとても簡単で、CopyOWriteのメカニズムを知る限り、様々なCopyOnWrite容器を実現し、異なるアプリケーションシーンで使用することができます。
CopyOWriteの応用シーン
CopyOnWriteを併発した容器は、読み書きの少ない併発シーンに使用されます。例えば、ホワイトリスト、ブラックリスト、商品類目の訪問と更新シーンがあります。もし検索サイトがあれば、ユーザーはこのウェブサイトの検索ボックスにキーワードを入力して内容を検索しますが、いくつかのキーワードは検索できません。これらの検索できないキーワードはブラックリストに入れられます。ブラックリストは毎晩更新されます。ユーザーが検索すると、現在のキーワードがブラックリストにないことを確認します。もしあるなら、ヒントは検索できません。実現コードは以下の通りです。
package com.king.book;

import java.util.Map;
import com.king.book.forkjoin.CopyOnWriteMap;

/**
 *      
 */
public class BlackListServiceImpl {

    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(1000);

    public static boolean isBlackList(String id) {
        return blackListMap.get(id) == null ? false : true;
    }

    public static void addBlackList(String id) {
        blackListMap.put(id, Boolean.TRUE);
    }

    /**
     *        
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
        blackListMap.putAll(ids);
    }
}
コードは簡単ですが、CopyOWriteMapを使うには二つのことに注意が必要です。
  • は、拡張オーバヘッドを低減する。実際の必要に応じて、CopyOWriteMapのサイズを初期化し、CopyOnWriteMapの拡大支出を避ける。
  • は大量添加を使用する。添加するたびに、容器は毎回複製するので、添加回数を減らし、容器の複製回数を減らすことができます。上のコードの中のaddBlackList方法を使うなら。
  • CopyOWriteの欠点
    CopyOWrite容器には多くの利点がありますが、同時にメモリの占有問題とデータの整合性の問題があります。だから開発には注意が必要です。
    メモリ占有問題
    CopyOWriteの書き込み時にコピーする仕組みなので、書き込み操作を行う際には、メモリには2つのオブジェクトのメモリが同時に存在し、古いオブジェクトと新しい書き込みの対象が同時に存在します。対象メモリが二つあります。これらのオブジェクトが占有するメモリが比較的大きい場合、例えば200 M前後であれば、さらに100 Mのデータを書き込み、メモリは300 Mを占めます。この場合、頻繁にYong GCとフルGCが発生する可能性があります。前のシステムではCopyOWriteを使って夜毎に大きなオブジェクトを更新し、毎晩15秒のフルGCをもたらしました。応用応答時間も長くなりました。
    メモリの占有問題に対しては、大きなオブジェクトのメモリ消費を圧縮する方法により、例えば、要素が全部10進の数字であれば、36進または64進に圧縮することが考えられます。CopyOnWrite容器を使わずに、他のコンカレントHashMapなどのコンカレント容器を使用します。
    データ整合性の問題
    CopyOWrite容器はデータの最終的な一致だけを保証して、データのリアルタイムの一致性を保証することができません。ですから、書き込むデータが欲しいなら、すぐ読めます。CopyOWrite容器を使わないでください。