ソースコードを確認する前に、ThreadLocalの機能を理解する必要があります。ThreadLocalは、異なるスレッド間で同じ変数を分離します。つまり、各スレッドは独自の独立したコピーを持つことができ、スレッドのメソッド間で共有できます(いつでも取り出すことができます)。使用する)。わからない場合は、記事の3番目の部分で使用例を確認できます。
これは実際には時空のアイデアです。各スレッドに独自の独立したコピーがある場合、スレッドの安全性を確保して時間を節約するために実行をシリアル化するためにスレッドをロックする必要はありませんが、それぞれのコストがかかります。各スレッドは独立したスペースを開きます。
ThreadLocalの機能を理解した後、ThreadLocalをどのように設計する必要がありますか?
1.スレッド分離を設計する方法
まず、各スレッドがThreadLocal用に個別のメモリを開く必要があると考えるのは簡単ですが、ThreadLocalと同じメモリを開くだけでは十分ではありません。スレッドには複数の独立したコピーがある場合があるため、言い換えると、ThreadLocalは次のような複数のクラスで作成できます。
public class A {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
public void f1(){
threadLocal1.set("test");
}
public class B {
private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
public void f2(){
threadLocal1.set(001);
}
次に、実行スレッドの場合、testと001はその独立したコピーであり、保存する必要があります。これらの違いは、特定のThreadLocalオブジェクトにあります。
次に、ThreadLocalをスレッド(Threadクラス)に保存する方法を見てみましょう。
各スレッドがThreadLocalMapを維持し、ThreadLocalMapに格納されているのは、エントリを要素として持つテーブル配列です(エントリはキーと値の構造:キーはThreadLocal、値は格納された値)であるため、次の2つの情報を取得できます。
- 配列は、各スレッドが複数の独立したコピーを格納できることを保証します
- エントリは、異なるコピーを区別する方法を提供します。つまり、ThreadLocalは異なります。
さらに、2つの変数がありますが、threadLocalsのみが直接設定/取得操作されます。親スレッドで子スレッドが作成された場合、親スレッドのinheritableThreadLocalsが子スレッドにコピーされます。
ソースコードを見る前に理解しておくべきロジックがついに完成しました。メインショーに入りましょう...
2.ThreadLocal
ThreadLocalコアメンバー変数とメインコンストラクター:
// ThreadLocal使用了泛型,所以可以存放任何类型
public class ThreadLocal<T> {
// 当前 ThreadLocal 的 hashCode,作用是计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
// nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
private final int threadLocalHashCode = nextHashCode();
// nextHashCode 直接决定 threadLocalHashCode(= nextHashCode++)
// 这么做因为ThreadLocal可能在不同类中new出来多个,但线程只有一个,若每次下标都从同一位置开始,虽然有hash碰撞处理策略,但仍然会影响效率
// static:保证了nextHashCode的唯一性,间接保证了threadHashCode唯一性
private static AtomicInteger nextHashCode = new AtomicInteger();
static class ThreadLocalMap{
...}
// 只有空参构造
public ThreadLocal() {
}
// 计算 ThreadLocal 的 hashCode 值,就是通过CAS让 nextHashCode++
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//......
}
2.1 set()
現在のスレッドのthreadLocalsを取得し、それにEntry(現在のThreadLocalオブジェクト、値)を入れます。さらに、設定操作の各スレッドはシリアルであるため、スレッドの安全性の問題は発生しません。
public void set(T value) {
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap,即threadLocals变量
ThreadLocalMap map = getMap(t);
// 当前 thradLocal 非空,即之前已经有独立的副本数据了
if (map != null)
map.set(this, value); // 直接将当前 threadLocal和value传入
// 当前threadLocal为空
else
createMap(t, value); // 初始化ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2.2 get()
現在のスレッドのtheadLocalsにある現在のThreadLocalオブジェクトに対応する値を取得します
- 現在のスレッドでthreadLocalsを取得します
- threadLocals = nullの場合、初期化します
- 現在のThreadLocalオブジェクトを介して対応するエントリを取得します
- エントリ!= null、結果を返す
- entry = null、nullを返す
public T get() {
// 拿出当前线程
Thread t = Thread.currentThread();
// 从线程中拿到 threadLocals(ThreadLocalMap)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中拿到相应entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否则给当前线程的 ThreadLocal 初始化,并返回初始值 null
return setInitialValue();
}
3.ThreadLocalMap
- これは内部クラスですが、ThreadLocalMapは、リストのコンポーネントであるリストのノードとは異なります(リスト>ノード)。
- ThreadLocalMapは、Threadを属性として指定し、ThreadLocalを保存するために使用されます(Thread> ThreadLocalMap> ThreadLocal)
// 静态内部类,可直接被外部调用
static class ThreadLocalMap {
// Entry(k,v)
// k = WeakReference 是弱引用,当没有引用指向时,会直接被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// 当前 ThreadLocal 关联的值
Object value;
// WeakReference 的引用 referent 就是 ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 存储 (ThreadLocal,Obj) 的数组
private Entry[] table;
// 数组的初始化大小
private static final int INITIAL_CAPACITY = 16;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
//.......
}
3.1 set()
エントリ(threadLocal、オブジェクト値)をthreadLocalsの配列に入れます
- threadLocalsの配列を取得します
- 現在のThreadLocalに対応する配列添え字を計算します
- エントリ(threadLocal、オブジェクト値)を配列に入れます
- ハッシュの衝突なし、新しいエントリを配置
- ハッシュコリジョンi ++がある場合は、エントリがない位置が見つかるまで、新しいエントリを配置します
- 同じキー(ThreadLocal)が見つかった場合は、値を置き換えます
- 拡張する必要があるかどうかを判断します
private void set(ThreadLocal<?> key, Object value) {
// 1.拿到当前threadLocals的数组
Entry[] tab = table;
int len = tab.length;
// 2.计算当前 ThreadLocal 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余
int i = key.threadLocalHashCode & (len-1);
// 可以看到循环的结束条件是 tab[i]==null,即无哈希冲突
// 若出现哈希冲突时,依次向后(i++)寻找空槽点。nextIndex方法就是让在不超过数组长度的基础上,把数组的索引位置 + 1
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到内存地址一样的 ThreadLocal,直接替换
if (k == key) {
e.value = value;
return;
}
// 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 当前 i 位置是无值的,可以被当前 thradLocal 使用
tab[i] = new Entry(key, value);
int sz = ++size;
// 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3.2 getEntry()
対応するノードエントリを取得します
- 現在のThreadLocalに対応するインデックス位置を計算します(アレイサイズ-1を法とするハッシュコード)
- e!= nullの場合、現在のエントリを返します
- e == nullまたはキー(ThreadLocal)が一致しない場合は、getEntryAfterMissスピンを呼び出して検索します
private Entry getEntry(ThreadLocal<?> key) {
// 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// e 不为空 && e 的 ThreadLocal 的内存地址和 key 相同
if (e != null && e.get() == key)
return e; // 直接返回
// 因为上面解决Hash冲突的方法是i++,所以会出现计算出的槽点为空或者不等于当前ThreadLocal的情况
else
return getEntryAfterMiss(key, i, e); // 继续通过 getEntryAfterMiss 方法找
}
getEntryAfterMiss:thradLocalMapが設定されたときに配列インデックス位置の競合を解決するロジックによると、このメソッドの検索ロジックも対応しています。つまり、見つかるまでi +1をスピンします。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 内存地址一样,表示找到了
if (k == key)
return e;
// 删除没用的 key
if (k == null)
expungeStaleEntry(i);
// 继续使索引位置 + 1
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3.3 resize()
ThreadLocalMapのThreadLocalの数がしきい値を超えると、ThreadLocalMapが拡張を開始します
- threadLocalsテーブルを取得します
- 新しい配列を初期化します。サイズは元の2倍です
- 古い配列を新しい配列にコピーします
- キー(ThreadLocal)に従って新しいインデックス位置を計算します
- ハッシュの衝突がある場合、i ++
- 新しい拡張しきい値を計算し、新しい配列をテーブルに割り当てます
private void resize() {
// 1.拿出旧的数组
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 2.计算新数组的大小,为老数组的两倍
int newLen = oldLen * 2;
// 初始化新数组
Entry[] newTab = new Entry[newLen];
int count = 0;
// 3.老数组的值拷贝到新数组上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 计算 ThreadLocal 在新数组中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果出现哈希冲突,即索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 给新数组赋值
newTab[h] = e;
count++;
}
}
}
// 4.计算新数组下次扩容阈值,为数组长度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}
4.使用例
次に、9つのサブスレッドを開始します。各サブスレッドは名前(thread-i)を同じ変数に格納し、それを出力します。同じ変数をスレッド間で分離できる場合(補完的な効果)、コンソールの正しい結果はスレッド0からスレッド8になるはずであると考えられます。
この変数の2つの実装を以下に示します。1。通常の変数String、2。ThreadLocal<String>
4.1通常の変数
public class StringTest {
// 保存线程名的普通变量value
private String value;
// 不直接设置value,而是暴露出get和set方法
private String getString() {
return string; }
private void setString(String string) {
this.string = string; }
public static void main(String[] args) {
StringTest test= new StringTest ();
int threads = 9; // 要启动的线程个数
CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束
// 启动9个子线程
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
test.setString(Thread.currentThread().getName()); // 向变量value中存入线程名 thread - i
System.out.println(test.getString()); // 然后打印出来。注:这里可能存在并发
countDownLatch.countDown(); // 门栓-1
}, "thread - " + i);
thread.start();
}
}
countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行
}
結果は次のとおりです。
thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8
スレッド0がないことがわかりますが、スレッド1は2回表示されるため、通常の変数を使用して同じ変数を異なるスレッドから分離することはできません。
4.2 ThreadLocal
ThreadLocalを使用する場合、通常は静的として宣言されます
- クラスごとに1つのThreadLocal->現在のスレッドに1つのエントリで十分です
- 使用時に簡単に呼び出す
public class ThreadLocalStringTest {
// 保存线程名的ThreadLocal变量threadLocal
// 注:这里除了是String,也可是别的任何类型(Integer,List,Map...)
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 不直接操作 threadLocal,而是封装成 set/get 方法
private String getString() {
return threadLocal.get(); }
private void setString(String string) {
threadLocal.set(string);}
public static void main(String[] args) {
ThreadLocalStringTest test= new ThreadLocalStringTest();
int threads = 9; // 要创建的子线程个数
CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束
// 创建 9 个线程
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
test.setString(Thread.currentThread().getName()); // 向ThreadLocal中存入当前线程名 thread - i
System.out.println(test.getString()); // 向ThreadLocal获取刚存的线程名。注:可能存在并发
countDownLatch.countDown(); // 门栓-1
}, "thread - " + i);
thread.start();
}
countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行
}
}
動作結果:
thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8
実行結果が期待どおりであることがわかります。つまり、ThreadLocalは、スレッド間の同じ変数の分離を実現します。さらに、ThreadLocalをシングルサインオンに使用して、さまざまな要求スレッドのトークンを保存し、解析中にそれらを取り出すこともできます。