ThreadLocalの復号化

15425 ワード

概要


読者はネット上でもThreadLocalに関する資料をたくさん見たと信じています.多くのブログでは、ThreadLocalはマルチスレッドプログラムの同時問題を解決するために新しい考え方を提供しています.ThreadLocalの目的は、マルチスレッドがリソースにアクセスする際の共有の問題を解決することです.もしあなたもそう思うなら、今10秒、クリアする前にThreadLocalに対する間違った認識をあげます!JDKのソースコードを見てみましょう.
This class provides thread-local variables. These variables differ fromtheir normal counterparts in that each thread that accesses one (via its{@code get} or {@code set} method) has its own, independently initializedcopy of the variable. {@code ThreadLocal} instances are typically privatestatic fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
翻訳して大体このような(英語がよくなくて、もっと良い翻訳があるならば、伝言を残して説明してください):
ThreadLocalクラスは、スレッド内部のローカル変数を提供するために使用されます.この変数は、マルチスレッド環境でアクセス(getまたはsetメソッドでアクセス)すると、各スレッド内の変数が他のスレッド内の変数とは相対的に独立することを保証します.ThreadLocalインスタンスは、通常、スレッドとスレッドのコンテキストを関連付けるためにprivate staticタイプである.
一つの言葉にまとめることができる:ThreadLocalの役割はスレッド内の局所変数を提供することであり、この変数はスレッドのライフサイクル内で機能し、同じスレッド内の複数の関数またはコンポーネント間のいくつかの共通変数の伝達の複雑さを減らす.例えば、私は外出するにはまずバスに乗ってから地下鉄を作る必要があります.ここのバスと地下鉄は同じスレッド内の2つの関数のようなものです.私は1つのスレッドです.私はこの2つの関数を完成するには同じものが必要です.バスカード(北京バスと地下鉄はバスカードを使用しています)では、私はこの2つの関数にバスカードという変数を伝えないために(バスカードをずっと持って行くのではなく)、私はこのようにすることができます:バスカードを事前に1つの機関に渡して、私がカードを払う必要があるときにこの機関にバスカードを要求します(もちろん毎回同じバスカードを持っています).これで私(同じスレッド)がバスカードを必要とする限り、いつでもどこでもこの機関に求めることができる目的を達成することができます.
バスカードをグローバル変数に設定してもいいですよ.そうすれば、いつでもどこでもバスカードを取ることができるのではないでしょうか.しかし、多くの人(多くのスレッド)がいたら?みんな同じバスカードを使ってはいけませんよね(バスカードが実名認証だと仮定します)、これでめちゃくちゃではないでしょうか.今分かったでしょう?これがThreadLocal設計の目的である:スレッド内部の局所変数を提供し、本スレッド内でいつでもどこでも取り、他のスレッドを隔離する.

ThreadLocal基本操作


コンストラクタ


ThreadLocalのコンストラクション関数署名は次のとおりです.
/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

内部は何もしていません.

initialValue関数


InitialValue関数は、ThreadLocalの初期値を設定するために使用されます.関数署名は次のとおりです.
protected T initialValue() {
    return null;
}

この関数は、get関数が呼び出されたときに初めて呼び出されますが、最初からset関数が呼び出された場合、その関数は呼び出されません.通常、この関数は、remove関数が手動で呼び出された後にget関数が呼び出されない限り、一度だけ呼び出されます.この場合、get関数ではinitialValue関数が呼び出されます.この関数はprotectedタイプで、サブクラスで関数を再ロードすることを推奨していることは明らかです.そのため、通常、関数は匿名の内部クラスとして再ロードされ、初期値を指定します.たとえば、次のようにします.
package com.winwill.test;

/**
 * @author qifuguang
 * @date 15/9/2 00:05
 */
public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(1);
        }
    };
}

get関数


この関数は、現在のスレッドに関連付けられたThreadLocalの値を取得するために使用されます.関数署名は次のとおりです.
public T get()

現在のスレッドにThreadLocalの値がない場合、initialValue関数が呼び出されて初期値が返されます.

set関数


set関数は、現在のスレッドのThreadLocalの値を設定するために使用され、関数署名は次のとおりです.
public void set(T value)

現在のスレッドのThreadLocalの値をvalueに設定します.

remove関数


remove関数は、現在のスレッドのThreadLocalバインド値を削除するために使用されます.関数署名は次のとおりです.
public void remove()

メモリの漏洩を防ぐために、関数を手動で呼び出す必要がある場合があります.

コードデモ


最も基本的な操作を学習した後、次のようなシーンを実現するコードを使用してThreadLocalの使い方を実証します.
5つのスレッドがあり、この5つのスレッドには1つの値valueがあり、初期値は0であり、スレッドの実行時に1サイクルでvalue値に数値を加算します.
コード実装:
package com.winwill.test;

/**
 * @author qifuguang
 * @date 15/9/2 00:05
 */
public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new MyThread(i)).start();
        }
    }

    static class MyThread implements Runnable {
        private int index;

        public MyThread(int index) {
            this.index = index;
        }

        public void run() {
            System.out.println(" " + index + " value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println(" " + index + " value:" + value.get());
        }
    }
}

実行結果:
スレッド0の初期value:0スレッド3の初期value:0スレッド2の初期value:0スレッド2の累加value:45スレッド1の初期value:0スレッド3の累加value:45スレッド0の累加value:45スレッド1の累加value:45スレッド4の初期value:0スレッド4の累加value:45
各スレッドのvalue値は互いに独立しており,本スレッドの累積操作は他のスレッドの値に影響を及ぼさず,本当にスレッド内部分離の効果を達成していることがわかる.

どのように実現するか


基本的な紹介を見て、最も簡単な効果のプレゼンテーションを見た後、私たちはもっとThreadLocal内部の実現原理をよく研究しなければならない.もしあなたにデザインをしたら、あなたはどのように設計しますか?多くの人がこのような考えを持っていると信じています.
ThreadLocalクラスごとにMapを作成し、スレッドのIDをMapのkey、インスタンスオブジェクトをMapのvalueとすることで、各スレッドの値分離の効果が得られます.
そう、これは最も簡単な設計案で、JDKの最も初期のThreadLocalはこのように設計されています.JDK1.3(1.3かどうかは不明)後にThreadLocalのデザインが変わりました.
まずJDK 8のThreadLocalのgetメソッドのソースコードを見てみましょう.
public T get() {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      return setInitialValue();
  }

ここでgetMapのソースコード:
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

setInitialValue関数のソース:
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

createMap関数のソースコード:
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

簡単に解析すると、getメソッドの流れは次のようになります.
  • 現在のスレッド
  • を最初に取得する.
  • 現在のスレッドから1つのMap
  • を取得する
  • 取得したMapが空でない場合、MapではThreadLocalの参照をキーとしてMapで対応するvalue eを取得し、そうでない場合は5
  • に進む.
  • eがnullでない場合はe.valueを返します.そうでない場合は5
  • に移動します.
  • Mapが空またはeが空の場合、initialValue関数によって初期値valueが取得され、ThreadLocalの参照とvalueをfirstKeyおよびfirstValueとして新しいMap
  • が作成される.
    次に、Threadクラスにメンバー変数が含まれていることに注意してください.
    ThreadLocal.ThreadLocalMap threadLocals = null;

    したがって、ThreadLocalの設計構想をまとめることができます:各ThreadはThreadLocalMapマッピングテーブルを維持し、このマッピングテーブルのkeyはThreadLocalインスタンス自体であり、valueは本当に記憶する必要があるObjectです.この案はちょうど私たちが話し始めた簡単な設計案とは反対です.資料を調べてみると、このような設計には主に以下の利点があります.
  • このように設計された後、各MapのEntry数は小さくなった:以前はThreadの数だったが、現在はThreadLocalの数であり、性能を向上させることができ、性能の向上は一点二点(親測なし)
  • ではないという
  • Threadが破棄されると対応するThreadLocalMapも破棄され、メモリ使用量を削減できます.

  • もっと深く


    まず、ThreadLocalMapはThreadLocalの弱い参照をKeyとして使用しているという事実を説明します.
    static class ThreadLocalMap {
    
            /**
             * The entries in this hash map extend WeakReference, using
             * its main ref field as the key (which is always a
             * ThreadLocal object).  Note that null keys (i.e. entry.get()
             * == null) mean that the key is no longer referenced, so the
             * entry can be expunged from table.  Such entries are referred to
             * as "stale entries" in the code that follows.
             */
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
            ...
            ...
    }

    次の図は、本明細書で説明したオブジェクト間の参照関係図です.実線は強参照、破線は弱参照です.
    そしてネット上では、ThreadLocalがメモリの漏洩を引き起こすと噂されています.彼らの理由はこうです.
    以上の図のように、ThreadLocalMapはThreadLocalの弱い参照をkeyとして使用し、1つのThreadLocalが外部から強く引用されていない場合、システムgcの場合、このThreadLocalは必ず回収され、これにより、ThreadLocalMapにkeyがnullのEntryが現れ、これらのkeyがnullのEntryのvalueにアクセスすることができず、現在のスレッドがこれ以上遅く終わらなければ、これらのkeyがnullのEntryのvalueには、Thread Ref->Thread->Thread->ThreaLocalMap->Entry->valueが回収されず、メモリが漏洩します.
    このような状況が発生するかどうか見てみましょう.実際、JDKのThreadLocalMapの設計では、このような状況を考慮し、いくつかの防護措置も加えられています.以下はThreadLocalMapのgetEntry方法のソースコードです.
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    getEntryAfterMiss関数のソースコード:
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
         Entry[] tab = table;
         int len = tab.length;
    
         while (e != null) {
             ThreadLocal<?> k = e.get();
             if (k == key)
                 return e;
             if (k == null)
                 expungeStaleEntry(i);
             else
                 i = nextIndex(i, len);
             e = tab[i];
         }
         return null;
     }
    expungeStaleEntry関数のソースコード:
    private int expungeStaleEntry(int staleSlot) {
               Entry[] tab = table;
               int len = tab.length;
    
               // expunge entry at staleSlot
               tab[staleSlot].value = null;
               tab[staleSlot] = null;
               size--;
    
               // Rehash until we encounter null
               Entry e;
               int i;
               for (i = nextIndex(staleSlot, len);
                    (e = tab[i]) != null;
                    i = nextIndex(i, len)) {
                   ThreadLocal<?> k = e.get();
                   if (k == null) {
                       e.value = null;
                       tab[i] = null;
                       size--;
                   } else {
                       int h = k.threadLocalHashCode & (len - 1);
                       if (h != i) {
                           tab[i] = null;
    
                           // Unlike Knuth 6.4 Algorithm R, we must scan until
                           // null because multiple entries could have been stale.
                           while (tab[h] != null)
                               h = nextIndex(h, len);
                           tab[h] = e;
                       }
                   }
               }
               return i;
           }

    ThreadLocalMapのgetEntry関数の流れを整理します.
  • は、まず、ThreadLocalの直接インデックス位置(ThreadLocal.threadLocalHashCode&(len-1)演算により得られる)からEntry eを取得し、eがnullでなくkeyが同じであればeを返す.
  • eがnullまたはkeyが一致しない場合は次の位置へクエリーし、次の位置のkeyが現在クエリーが必要なkeyと等しい場合は対応するEntryを返し、そうでない場合はkey値がnullの場合はその位置のEntryを消去し、そうでない場合は次の位置へ
  • を問い合わせる.
    この過程で出会ったkeyがnullのEntryが消去されると、Entry内のvalueにも強い参照チェーンがなく、自然に回収されます.コードをよく調べると、setの操作にも似たような考えがあり、keyがnullのこれらのEntryを削除し、メモリの漏洩を防ぐことができます.しかし、それだけでは十分ではありません.上の設計構想は、ThreadLocalMapのgetEntry関数またはset関数を呼び出すという前提条件に依存しています.これはもちろん何も成り立たないので、使用者がThreadLocalのremove関数を手動で呼び出し、不要になったThreadLocalを手動で削除し、メモリの漏洩を防ぐ必要がある場合が多い.だからJDKはThreadLocal変数をprivate staticと定義することを提案して、このようにすればThreadLocalのライフサイクルはもっと長くて、ずっとThreadLocalの強い引用が存在するため、ThreadLocalも回収されないで、いつでもThreadLocalの弱い引用によってEntryのvalue値にアクセスすることができることを保証することができて、それからremoveそれを使って、メモリの漏洩を防ぐことができます.

    宣言


    オリジナルの文章、転載は出典を明記してください、本文のリンク:http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/