リード・ライト・ロック・チューニング・キャッシュ・オブジェクトの同時同期問題の説明思考分析

13380 ワード

ビジネス上の問題


共有Mapオブジェクトを使用したマルチスレッドのローカルキャッシュパフォーマンスの問題を最近最適化しました.従来の実装の背景は、MapオブジェクトがRedisからロードしたデータを格納することであり、対応するRedisデータが空の場合、Redisロードロジックを呼び出す必要があり、このロジックは更新データメソッドにカプセル化され、同期ロックが追加され、スレッドセキュリティが実現される.
サンプルコード:
private Map<String,Object> cachMap = Maps.newHashMap();
public synchronized void updateCache(Map<String,Object> getNewMap){
		cachMap.clear();
		cachMap.putAll(getNewMap);
	}
	public synchronized Map<String,Object> getCache(){
		return cachMap;
	}

 
synchronizedは共有オブジェクトの同期処理を実現するための第一選択の解決方法であることはよく知られており、処理スレッドが少ないリソース競合の場合、確かに性能に大きな影響はありませんが、現在私たちが直面しているシーンは、cacheの読み取り要求が10倍ほど増加し、書き込みデータの操作がほぼ変わらず、応用性能が著しく低下していることです.リンク全体の同時実行効率に影響します.
 

最適化シナリオ

  • Java 5の読み書きロックReadWriteLockを導入して読み書き共有を実現し、読み書きは反発特性を維持して読み取り性能を向上させる.

  • ロックにより、読み取りと書き込みの共有、読み取りと書き込みの反発、書き込みの反発を実現できます.最適化の例は次のとおりです.
    private ReadWriteLock cacheRWLock = new ReentrantReadWriteLock();
    	public  void updateCache(Map<String,Object> getNewMap){
    		try {
    		cacheRWLock.writeLock().lock();//    ,        
    		cachMap.clear();
    		cachMap.putAll(getNewMap);
    		} catch (Exception e) {
    			// TODO: handle exception
    		}finally{
    			cacheRWLock.writeLock().unlock();//    
    		}
    	}
    	public  Map<String,Object> getCache(){
    		try {
    			cacheRWLock.readLock().lock();//    ,       
    			return cachMap;
    		} catch (Exception e) {
    			// TODO: handle exception
    		}finally{
    			cacheRWLock.readLock().unlock();//    ,      
    		}
    		return null;
    	}
    

     
    解析コードの例では、共有オブジェクトMapを変更する前に書き込みロックを取得し、スレッドが取得されない場合は待機します.書き込みロックを取得してからデータ操作を開始し、データ操作後に書き込みロックを解除します.一方、読み出し対象Mapの前にリードロックを取得し、同時に複数のスレッドがリードロックを取得して並列の目的を達成することができる.同じスレッドについても、ライトロックが取得されると、同様にリードロック、すなわちAPI記述の再入力性を取得することができる.リードロックとライトロックはReadWriteLockで異なるロックメカニズムを採用しており、具体的な原理は表ではなく、APIには詳細な説明がある.    2. HashMapをConcurrentHashMapに置き換え、細粒度スレッドのセキュリティと同時パフォーマンスの向上を実現します.シーン解析から,我々が処理する必要がある共有オブジェクトはHashMapインスタンスであり,HashMapクラスは同時シーンではスレッドなしで安全であることが分かった.だがconcurrentはスレッドの安全なConcurrentHashMap実装を提供し、このクラスの実装は細粒度のhash segment次元の同期メカニズム、すなわち異なるsegmentが異なるロックに対応することを採用し、このようにして同期反発下の同時処理能力を維持することができる.具体的には,ConcurrentHashMapのclearとputAll実装ソースコードを解析する.
    public void clear() {
            final Segment[] segments = this.segments;
            for (int j = 0; j < segments.length; ++j) {
                Segment s = segmentAt(segments, j);
                if (s != null)
                    s.clear();
            }
    }
    static final  Segment segmentAt(Segment[] ss, int j) {
            long u = (j << SSHIFT) + SBASE;
            return ss == null ? null :
                (Segment) UNSAFE.getObjectVolatile(ss, u);
        }
    

     
    Clearの実装プロセスはmapオブジェクトのすべてのsegmentsを遍歴し、各segmentが持つvolatileで定義されたロックオブジェクトに対して同期ロックがあるかどうかを判断し、いずれもヒットしなければクリーンアップを実行することができる.
    public V put(K key, V value) {
            Segment<K,V> s;
            if (value == null)
                throw new NullPointerException();
            int hash = hash(key);
            int j = (hash >>> segmentShift) & segmentMask;
            if ((s = (Segment<K,V>)UNSAFE.getObject  
                 (segments, (j << SSHIFT) + SBASE)) == null) 
                s = ensureSegment(j);
            return s.put(key, hash, value, false);
        }
    

     
    PutAllの実装の基礎はputメソッドであり,基本原理は受信データkeyに対してhashを行い,ヒットしたsegmentに対して同期ロックが取得できるか否かを判断し,割当てsegments書き込みが取得できれば待つ.
    2つのシナリオと比較して、パフォーマンスは全体的に大きく異なりません.テストcaseは表にありません.

    派生思考の最適化


    読み書きロックを使用してコンカレントパフォーマンスシーンを最適化するには、読み取り量が変更量より大きいことを前提に、読み取り量が変更量とほぼ同じである場合、ビジネスの具体的な状況に応じて適切なポリシーを採用する必要がありますが、一般的なソリューションはありますか?

    CopyOnWriteスキームの試行


    Syncronizedキーワードに沿ってヘビー級ロックを実装するだけでなく、volatileを使用してメモリ共有インスタンスを実装することもできますが、メモリを頻繁に操作するコストが高いのは適切ではありません.それはvolatileとReentrantLockを組み合わせてCopyOnWriteメカニズムを実践し、CopyOnWriteArrayList類似機能を実現できるかどうか(java.util.concurrentはCopyOnWriteMapクラスを実現していないので、ずっと分かりにくい).CopyOnWriteMapの実装方法(ソースmongo-java-driver)を分析し、シーンを実装するために必要なclearとputAllのメソッドコードを以下のように仮定することができます.
    private volatile M delegate;// volatile     Map  
    private final transient Lock lock = new ReentrantLock(); //     
    

     
    上記の2つの重要な同期変数定義に基づいてclearメソッドはロックを優先的に取得し、置換メソッドを採用し、空のMapオブジェクトをdelegateストレージのMapを変更し、volatileに基づいて現在のスレッドがキャッシュからメモリにデータを書き込むことで他のスレッドキャッシュが失効する特性を置き換え、データ変更の同期を維持する.
    public final void clear() {
            lock.lock();
            try {
                set(copy(Collections. emptyMap()));
            } finally {
                lock.unlock();
            }
        }
    

     
    PutAllメソッドでは、読み書きロックを取得し、delegateに格納されている古いMapオブジェクトcopyを書き込むとともに、新しく入力されたオブジェクトcopyを中間Mapオブジェクトに、最後に中間Mapオブジェクトをdelegateに書き込むのが一般的です.対応するdelegate同期実装もJVMに任せる.操作が完了したら、読み書きロックを解除します.
    public final void putAll(final Map extends K, ? extends V> t) {
            lock.lock();
            try {
                final M map = copy();
                map.putAll(t);
                set(map);
            } finally {
                lock.unlock();
            }
        }
    

     
    この実装解析から,CopyOnWriteMapはvolatileストレージのオブジェクトに非常に依存しているが,我々のキャッシュには制限があるに違いないので,このスキームには処理Mapサイズを制限する必要があり,使用前に予測ストレージ量を評価する必要がある.そうしないとmapが大きすぎるとメモリオーバーフローを招きやすい.