Guavaキャッシュの実装原則の分析

I.概要

前回の記事「GuavaCacheがローカルキャッシュを実行する」では、Guava Cacheのローカルキャッシュとしての一般的な使用法を紹介し、LoadingCacheの構築方法とその基本的な使用法、および2つのgetメソッドの違いとの違いについて説明しました。 LoadingCacheとCacheおよびその他のコンテンツ。理由と理由を知るために、この記事ではGuava Cacheの実装原理を簡単に紹介します。何か問題がある場合は、それを批判して修正してください。

まず、GuavaCacheのソースリンクをgithubに出力します。作者が書いたメモがあり、誰もが学ぶのに便利です。
Guavaキャッシュの注釈付きソースコード

2.懸念事項

上記のソースコードとGuavaCacheの使用法に関する前回の記事から、Guava Cacheには多くのクラスとコードが含まれていることがわかります。ファイルを1つずつ分析すると、詳細が多すぎて、そうではありません。重要な問題を簡単に把握できます。そこで著者は、getメソッドの実行プロセスからGuavaCacheの原理を分析することにしました。分析は主に次の3つの問題に焦点を当てています。

  1. GuavaCacheがキーを介して値にマッピングする方法。
  2. キャッシュが失われたときにCacheLoader.loadメソッドを呼び出してキャッシュをロードする方法。
  3. 2つのキャッシュ無効化戦略はどのように実装されていますか?

3.ソースコード分析

3.1キー値マッピングプロセス

次の例を通して、Key-Valueマッピングプロセスを見てみましょう。

    public static void demo1() throws Exception {
    	//定义Cache,并定义未命中时的载入方法
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().build(new CacheLoader<Integer, Integer>() {
            @Override
            public Integer load(Integer key) throws Exception {
                return key + 1;
            }
        });
        cache.get(1); //第一个get:第一次访问key,使用载入方法载入
        cache.get(1); //第二个get:第二次访问,在缓存中得到value
    }

上記のデモでは、LoadingCacheを定義し、CacheLoaderを渡して、キャッシュが失われた場合の処理​​方法を提供します。後で、getメソッドが2回呼び出されていることがわかります。最初は、loadメソッドを介して対応する値をキャッシュにロードします。このプロセスはセクション3.2で紹介されています。このセクションでは、主に2番目の取得プロセス(対応する値がキャッシュに存在する場合にキーを介して値を取得する方法)について説明します。

デバッグブレークポイントを介して、getメソッドに入り、その実行プロセスを観察します。

ステップ1:

最初のエントリは次のメソッドです。これは、LoaclLoadingCacheが含まれているLoaclLoadingCacheがLoadingCacheインターフェイスのデフォルトの実装であると推測できます。このクラスはLoaclCacheの内部クラスです。getメソッドはgetOrLoadメソッドを直接呼び出します。

static class LocalLoadingCache<K, V> extends LocalCache.LocalManualCache<K, V> implements LoadingCache<K, V> {
        public V get(K key) throws ExecutionException {
            return this.localCache.getOrLoad(key);
        }
}

ステップ2:
getOrLoadメソッドを見下ろし続けると、それがカプセル化のレイヤーにすぎないことがわかります。続けてください。

    V getOrLoad(K key) throws ExecutionException {
        return this.get(key, this.defaultLoader);
    }

ステップ3:
LocalCache.getメソッドまで追跡を続けます。いくつかの操作は最終的にここで見ることができます。全部で2つの文があります、それらを別々に分析してください。

最初の文はハッシュ値を見つけることです。ここで、Preconditions.checkNotNullメソッドを使用してnullポインターをチェックします。キーがnullの場合、nullポインター例外がスローされ、ハッシュ操作が実行されます。ハッシュ演算では、まずkey.hashCode()の戻り値を取得してから、再ハッシュ演算を実行します。再ハッシュの目的は、key.hashCode()の品質の低下によって引き起こされる深刻なハッシュの競合を防ぐことです。私が理解している具体的な方法は、一連のビット演算を実行して、ハッシュ値の高低を混同することです。

2番目の文は分析の焦点です。ステップ4を参照してください。

    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = this.hash(Preconditions.checkNotNull(key));
        return this.segmentFor(hash).get(key, hash, loader);
    }

ステップ4:
LoadingCacheは、ConcurrentHashMapと同様の方法を使用して、マッピングテーブルを複数のセグメントに分割します。セグメントには同時にアクセスできるため、同時実行の効率が大幅に向上し、同時競合の可能性を減らすことができます(セグメント上のキーに同時にアクセスすると、競合が発生する可能性があります)。

手順3のコードの最後の行は、2つの部分に分けることができます。1つはthis.segmentFor(hash)です。これは、ハッシュ値を介してキーが配置されているセグメントを判別し、セグメントを取得します。代わりに、セグメントを介してgetメソッドを実行し、特定の値を取得します。

最初にthis.segmentFor(hash)のコードを見てください:

LocalCache.Segment<K, V> segmentFor(int hash) {
    return this.segments[hash >>> this.segmentShift & this.segmentMask];
}

セグメントが配列に格納されていることがわかります。配列の長さは2の累乗であり、LoadingCacheの同時実行レベルと同じです。では、segmentShiftとsegmentMaskはどのように値を取りますか?SegmentMashは、配列の長さから1を引いたものとして理解できるため、AND演算後の範囲は常に配列の長さの範囲内になります。segmentShiftの値に興味がある場合は、ソースコードLocalCacheの構築方法を参照できるので、ここでは繰り返しません。

セグメントを取得した後、最終的なgetメソッドを実行できます。コアメインコードは次のとおりです。

	   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

コードReferenceEntry <K、V> e = this.getEntry(key、hash);は、キーに関連するキーと値のペアを取得し、後続のコードは、有効期限戦略(セクション3.3で紹介)やロード操作(セクション3.3で紹介)などの操作を実行します。 3.2で導入)。ここでは、主にgetEntryメソッドについて説明します。

HashMapなどと同様に、セグメントも配列とリンクリストのデータ構造を使用します。まず、ハッシュ値と配列の長さをマイナス1にして、リンクリストのヘッドノードを取得します。次に、リンクリストをトラバースすることで実際の結果を取得します。

まとめ
上記はLocalCacheのgetメソッドの分析です。その実装メソッドは、いくつかの詳細を除いて、ConcurrentHashMapなどのJava標準クラスライブラリでの実装と基本的に類似していることがわかります。ステップ4の分析から、get操作の実行時にCacheLoader.loadが呼び出され、キャッシュ無効化の判断が実行されることもわかります。これら2つのコンテンツの原則を見てみましょう。

3.2CacheLoader.loadの呼び出しメカニズム

最初に前の記事の知識を確認してください。CacheLoader.loadはLoadingCacheのビルド時にCacheBuilderに渡され、キャッシュミスの場合に呼び出されます。セクション3.1のステップ4で、Loadが呼び出されるエントリポイントを確認しました。読みやすくするために、ここにコードを投稿します。

	   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

this.count == 0の場合、最後から2番目の行のthis.lockedGetOrLoadメソッドが実行され、このメソッドでCacheLoader.loadが呼び出されることがわかります。これは書き込み操作であるため、セグメントをロックする必要があります。セグメントはReentrantLockのサブクラスであり、そのlock()メソッドを直接呼び出すことでロックできます。次に、CacheLoader.load()を呼び出して値を取得し、テーブルのリンクリストに配置します。特定のコードはより面倒であり、分析されなくなります。

3.32つのキャッシュ有効期限戦略の実装

Guava Cacheは、2つの有効期限戦略を設定できます。1つは、読み取りキャッシュが単位時間内に期限切れにならないこと、もう1つは、書き込みキャッシュが単位時間内に期限切れにならないことです。

有効期限が切れているかどうかを判断するためのエントリは、セクション3.1のステップ4で説明されているsegment.getにあります。

   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               ……
           }
           ……
       }

getLiveValueメソッドを最後までたどると、次の判断の位置がわかりました。

    boolean isExpired(ReferenceEntry<K, V> entry, long now) {
        Preconditions.checkNotNull(entry);
        if (this.expiresAfterAccess() && now - entry.getAccessTime() >= this.expireAfterAccessNanos) {
            return true;
        } else {
            return this.expiresAfterWrite() && now - entry.getWriteTime() >= this.expireAfterWriteNanos;
        }
    }

有効期限ポリシー、現在時刻、設定された有効期限に従って有効期限判定が行われているかどうかを確認できます。同時に、Guava Cacheの有効期限の判断は、別のスレッドを開くことではなく、読み取りおよび書き込み操作時に有効期限を判断することであることもわかっています。

パブリックアカウントに注意を払い、初めて私からより多くの記事を受け取ります
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/vxzhg/article/details/102648232