ThreadLocal詳細(AtomicInteger関連)


なお、本稿では、HashMapのソースコードを熟知することを前提として説明するが、HashMapのデータ構造を熟知していない場合は、HashMapを先に理解してから理解することができる.
本文はJDK 1.7で説明する.Handlerメカニズムを学習する過程で、Looperのメンバー属性メッセージキューMessageQueueはThreadLocalによって実現された各スレッドにはLooperオブジェクトが1つしかなく、各LooperオブジェクトにはMessageQueueオブジェクトが1つしかないことが分かった.実際には、通常、ThreadLocalをスレッド分離のツールとして使用している.AndroidのHandlerメカニズムに触れたことがない友人は、次の内容はあなたの理解に影響しません.
ソースコードを分析するには矢を放って、問題を持ってThreadLocalソースコードを分析します:まず問題を提出します:どうしてThreadLocalはスレッドの隔離のツールとすることができますか?問題が提起された後、ThreadLocalの分析を開始し、一般的な2つのメンバーメソッドをリストします.
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

以上の2つの方法を分析します.
1. ThreadLocal#set(T value):
  • Thread t = Thread.currentThread(); 現在のスレッド
  • を取得
  • ThreadLocalMap map = getMap(t);現在のスレッドの対応するThreadLocalMapオブジェクトを取得し、getMap(t)メソッドは、
  • として実装される.
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
  • 当map!=null; またはmap==null;map.set(this,value)をそれぞれ実行します.とcreateMap(t,value);メソッドは、set(T value)メソッドを最初に実行するとローカル変数mapがnullになるに違いないのでcreateMap(t,value)が実行されます.
  • /**
     *           HashMap     
     */
    private void set(ThreadLocal key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        //     threadLocalHashCode
        int i = key.threadLocalHashCode & (len-1);//      
    	//     table     i          (      ) key      Entry  ,    Entry   value       (      )value,  return     
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal k = e.get();
    
            if (k == key) {
                e.value = value;//    
                return;
            }
    
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        //  for       key      Entry  ,       Entry  ,    table   
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        //threadLocalHashCode    
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //ThreadLocal    key, firstValue  value   Entry 
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    

    createMap(Thread t,T firstValue)が実行されたことを見ると、現在のThread対応するThreadLocalMapオブジェクトthreadLocalsはnullではなくなっている.この方法でも明らかなように、ThreadLocalMapはThreadLocalオブジェクトをkeyとして伝達された値を保存している.注目すべきは、この方法が同じスレッドで複数回呼び出されると、後ろのオブジェクトは、前に設定するオブジェクトを置き換えます.
    2.ThreadLocal#get()メソッドの解析:このメソッドは簡単です.
  • List itemは、get()メソッドの実行前にset(T value)メソッドを先に実行すると、ローカル変数mapがnullにならないため、set(T value)メソッドから伝わるvalue値を取得する.
  • .
  • set()メソッドを先に実行しない場合、get()メソッドは最終的にsetInitialValue()メソッドを実行し、最終的にnull値を返す.
    ThreadLocal#set(T value)とThreadLocal#get()の2つの方法の分析を統合すると、次のことがわかります.
    なぜThreadLocalはスレッド分離のツールとして使用できるのか:-setのたびに、そのスレッドのオブジェクトThread.-各Threadは唯一のThreadLocalMapオブジェクトにのみ対応する.-このThreadLocalMapオブジェクトによってset(T value)を保存する渡された値.-このThreadLocalMapオブジェクトによって対応するvalue値が取得されます.-以上は1つのスレッドに対応する1つのThreadLocalMapオブジェクトのみが保証され、1つのスレッドのクラスが1つのインスタンスしか保証されていませんが、1つのスレッドのクラスがずっと同じインスタンスであることは保証されません.-したがって、あるスレッドが唯一の不変のインスタンスしか必要でない場合、たとえば、スレッドに1つのクラスのオブジェクトしかないクラスを定義します.
    /**
     *    Looper
     */
    public class Looper {
        /**
         *   
         */
        final static ThreadLocal<Looper> testThreadLocal = new ThreadLocal<>();
    
        /**
         *    new
         */
        private Looper() {
    
        }
    
        /**
         *            
         */
        public static void prepare() {
            // set   get
            if (testThreadLocal.get() != null) {
                throw new RuntimeException("          prepare()  ");
            }
            testThreadLocal.set(new Looper());
        }
    
        /**
         *            
         *
         * @return
         */
        public static Looper myLooper() {
            return testThreadLocal.get();
        }
    }
    

    以上の設計は、1つのThreadが1つのThreadLocalに対応することを保証する、また、1つのThreadが1つのクラスのインスタンスにのみ対応することを保証する.
    PS:前述のThreadLocalはHashMapの真髄を吸収し、特殊なHashMapであるが、HashMapではhashを計算する問題があるが、ThreadLocalのhash値は?これは、ThreadLocalの一般メンバーthreadLocalHashCodeと関連する3つのstaticメンバーを分析します.
    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
  • staticメンバーはクラスに属し、オブジェクトに属していないので、一度だけロード(初期化)します.
  • nextHashCodeというAtomicIntegerインスタンスは、メンバー属性valueがあり、無パラメトリック関数を使用して作成される場合、ここでは初期デフォルト値は0.
  • です.
  • threadLocalHashCodeはThreadLocalのhashCodeに相当し、ThreadLocalはこの値によってスレッド分離を実現する.
  • new ThreadLocalオブジェクトが出るたびにthreadLocalHashCodeが再割り当てされます.
  • トラッキングコードは、AtomicIntegerクラスを呼び出す方法を示す:
  • public final int getAndAdd(int delta) {
        for (;;) {
            //      ,       0(  )
            int current = get();//  AtomicInteger#value
            //     
            int next = current + delta;
            //  current == AtomicInteger#value,    AtomicInteger#value = next
            if (compareAndSet(current, next))
                return current;//   AtoomicInteger#value 
        }
    }
    

    メソッド分析:
  • getAndAdd(int delta)メソッドの役割は、デッドサイクルがThreadLocal#threadLocalHashCode.
  • に値を与えることである.
  • ThreadLocalのメンバー属性nextHashCodeはstaticであるため、すべてのThreadLocalオブジェクトは同じAtomicIntegerオブジェクトを共有し、このAtomicIntegerオブジェクトのvalue初期デフォルト値は0であり、forループでは:
  • まずvalueの値を取得し、currentを付与して保存する.
  • は次に、所望のnext=current+deltaを設定する.
  • そしてcompareAndSetメソッドを呼び出すことによって比較および交換する.
  • .
  • このプロセスにおいてvalueが変更されない場合、compareAndSet(current,next)が実行されるとcurrentはvalueに等しい.
  • ではvalue,value=nextが付与される.currentを返し、ThreadLocal#threadLocalHashCodeに値を割り当てます.
  • このとき、このThreadLocalオブジェクトのメンバー属性threadLocalHashCodeの値は0である.
  • ThreadLocalオブジェクトを再作成すると、すべてのThreadLocalオブジェクトがAtomicIntegerオブジェクトを共有しているため、この新しく作成されたThreadLocalオブジェクトのthreadLocalHashCodeメンバー属性は上記の方法を継続して実行します.唯一の違いは、このAtomicIntegerオブジェクトのvalueの値が前回の交換後の値(ポイント)であることです.
  • 上記のプロセスは、複数のThreadLocalオブジェクトがあっても、彼らのthreadLocalHashCodeが異なることを保証しています.


  • getAndAdd(int delta)は最後にcompareAndset()メソッドを呼び出します.
    public final boolean compareAndSet(int expect, int update) {
    	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    最終的にはUnsafeクラスのcompareAndSwapInt()メソッドが呼び出され,CAS(CompareAnd Swap)オペレーションが原子オペレーションによって実現され,compareAndSwapInt()メソッドは最後に追跡され,最下層はアセンブリ言語に基づいて実現される.プログラミングでは、「原子」が最小の単位を表すように設定されているので、原子操作は、実行が完了するまで他のタスクやイベントに中断されない最小の実行単位と見なすことができます.
    AtomicIntegerクラス関連テスト:
    /**
     * Created by tgvincent on 2019/7/8.
     *     ,   {@link java.util.concurrent.atomic.AtomicInteger}
     */
    public class AtomicIntegerTest {
        //volatile num     (         )
        private volatile int num = 10;
    
        public static void main(String[] args) {
            AtomicIntegerTest test = new AtomicIntegerTest();
            int temp = test.num;
            System.out.println("test.num: " + test.num);
            System.out.println("temp: " + temp);
            AtomicInteger atomicInteger = new AtomicInteger(test.num);
            new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    test.num++;
                }
            }).start();
    
            try {
                Thread.sleep(3000);//  3           
                System.out.println("=================After thread execution=================");
                System.out.println("test.num: " + test.num);
                System.out.println("temp: " + temp);
                boolean cas = atomicInteger.compareAndSet(test.num, 999);
                int result = atomicInteger.get();
                /**
                 * test.num     1010, != 10,   , {@link AtomicInteger#compareAndSet(int, int)}
                 *       ,   {@link AtomicInteger#get()}       10
                 */
                System.out.println("is swap?: " + cas +", result: "+ result);
                /**
                 * temp 10, == 10,   , {@link AtomicInteger#compareAndSet(int, int)}
                 *     ,     , {@link AtomicInteger#value} 999,
                 *   {@link AtomicInteger#get()}      999
                 */
                cas = atomicInteger.compareAndSet(temp, 999);
                result = atomicInteger.get();
                System.out.println("is swap?: " + cas +", result: "+ result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    テスト結果:
    test.num: 10
    temp: 10
    =================After thread execution=================
    test.num: 1010
    temp: 10
    is swap?: false, result: 10
    is swap?: true, result: 999
    

    補足質問:なぜ0 x 61 c 88647ですか?以下はネット検索の資料です.
  • はhash codeギャップをこの魔数として生成し,生成した値あるいはThreadLocalのIDを2のべき乗サイズの配列に比較的均一に分布させることができる.
  • は、前に構築されたThreadLocalのID/threadLocalHashCodeに魔数0 x 61 c 88647を加えたものであることがわかる.
  • という魔数の選択はフィボナッチハッシュに関係し,0 x 61 c 88647に対応する十進法は1640531527であった.
  • フィボナッチハッシュの乗数は(long)(1 L<<31)*(Math.sqrt(5)−1))で2654435769を得ることができ、この値を符号付きintに変換すると-164053527を得ることができる.すなわち(1 L<<32)−(long)((1 L<<31)*(Math.sqrt(5)−1))得られた結果は、1640531527、すなわち0 x 61 c 88647である.
  • 理論と実践により,0 x 61 c 88647を魔数としてThreadLocal毎にそれぞれのID,すなわちthreadLocalHashCodeを加算して2のべき乗と型をとると,得られた結果分布は均一であった.
  • ThreadLocalMapは線形検出法を使用しており、均一分布の利点は、次の近い利用可能なslotをすぐに検出し、効率を保証することである.効率を最適化するために.