26:スレッドローカル変数-ThreadLocal

この記事は、「新人クリエーションセレモニー」イベントに参加し、一緒にゴールドクリエーションの道を歩み始めました。

ThreadLocalは、スレッド内でローカル変数を提供するためにJDK1.2によって導入されたクラスです。通常の変数とは異なり、スレッドローカル変数はスレッドにバインドされ、各スレッドには独自の独立変数コンテナがあります。スレッドローカル変数は、スレッドのライフサイクル中に機能し、スレッド内の複数の関数またはコンポーネント間で共通変数を転送する複雑さを軽減するために使用されます。

使用例

public class ThreadLocalDemo {

    static final ThreadLocal<String> TL_DEMO = new ThreadLocal<>();
    public static void main(String[] args) {
        Runnable rn1 = new Runnable() {
            @Override
            public void run() {
                setVal("R1 TEST STRING");
                printVal();
            }
        };
        Runnable rn2 = new Runnable() {
            @Override
            public void run() {
                setVal("R2 TEST STRING");
                printVal();
            }
        };
        new Thread(rn1).start();
        new Thread(rn2).start();
    }
    
    public static void setVal(String val) {
        TL_DEMO.set(val);
    }

    public static void printVal() {
        System.out.println(TL_DEMO.get());
    }
}
复制代码

演算結果:

R1 TEST STRING
R2 TEST STRING
复制代码

これを見て、あなたは言うかもしれません、私はこれを達成するためにMapを使うことができます、なぜThreadLocalを使うのですか?まず、ThreadLocalはスレッドにバインドされます。各スレッドは互いに独立しており、相互に影響を与えません。現在のスレッドにバインドされているスレッドローカル変数は、ThreadLocalを介してどこからでも取得できます。もちろん、これはスレッドIDのマップをキーとして使用することによっても実現されます。次に、最も重要なことは、ThreadLocalに格納されているスレッドローカル変数がスレッドの破棄とともに自動的にリサイクル可能な状態になり、Mapを使用するには、スレッドが終了する前にマップクリア操作を表示する必要があります。そうでない場合、変数は常に強いつながりがあり、リサイクル状態に入ることができません。

ThreadLocalによって実装される関数は他の方法で実装できますが、ThreadLocalほど単純ではありません。ツールクラスの存在の意味はここにあります。複雑な関数を統合し、それらを使用する簡単な方法を提供し、プログラミングとコーディングを容易にします。簡単。明確。

実装分析

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}
复制代码

上記のsetメソッドを使用すると、Threadの実際の実装がMapを介して実装され、属性threadLocalsがThreadの内部で維持されていることがわかります。

ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

ThreadLocalMapのコンテナーはThread$ThreadLocalMap $ Entry []であり、参照関係を次の図に示します。

そして、Thread $ ThreadLocalMap $ Entryの場合:

private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
	Object value;

	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}
复制代码

EntryはWeakReferenceを継承していることがわかります。これは、EntryのThreadLocalへの参照が弱い参照であることを意味します。つまり、保持している強いThreadLocal参照がなくなると、ThreadLocalインスタンスはリサイクル可能な状態になります。ただし、Entryインスタンスはリサイクル状態になりません。

对于普通线程因为ThreadLocalMap维护在Thread内部,因此当Thread死亡时,其维护的线程本地变量容器ThreadLocalMap也会进入可回收状态。但是对于线程池线程,我们知道线程池运行过程中线程池的核心线程在未设置的情况下是不会销毁的,这意味着线程维护的ThreadLocalMap在线程池运行期间会一直存在,此时可以通过手动调用ThreadLocal.remove()来使Entry实例的强引用消失进入可回收状态。

但是也并没有一个地方说使用ThreadLocal需要强制调用ThreadLocal.remove()方法呀,这样一直不回收又一直有新对象进来的话为什么不会造成OOM呢?这是因为ThreadLocal会有一个自动的懒清理方式。ThreadLocalMap的存储方式不同于HashMap采用的拉链法,而是采用的开地址法

/**下标确认*/
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)])
/**下标确认*/

private static int nextIndex(int i, int len) {
	return ((i + 1 < len) ? i + 1 : 0);
}
复制代码

遭遇Hash冲突且发现存在Key已经被回收的无效Entry实例时,则会尝试性的进行一部分容器槽的扫描清除操作(不仅仅局限于冲突的那个,而是一定范围的扫描),使得Entry进入可回收状态。

那么为什么不将value也设置成弱引用呢?这样不是就可以不用手动执行ThreadLocal.remove(),达到像ThreadLocal实例一样的自动回收呢?这是因为ThreadLocal无法确定程序逻辑会在什么时间断开对value实例的强引用持有,如果将value也设置成弱引用,那么在程序逻辑失去强引用持有后,value就会在下一次GC时被回收,但是也许此时程序还持有ThreadLocal实例,并在接下来的运行中通过ThreadLocal实例去获取线程本地变量value,然后此时value却已被GC,造成意料之外的错误。

尽管如此,线程池模式下使用ThreadLocal依然建议在合适的地方调用ThreadLocal.remove()进行对象的及时回收,避免造成内存泄漏。

PS:
开发成长之旅 [持续更新中...]
上篇导航:25:线程间通信协作-Exchanger - 掘金 (juejin.cn)
下篇导航:27:JAVA中的各种锁 - 掘金 (juejin.cn)
欢迎关注…

おすすめ

転載: juejin.im/post/7079762570864754695
おすすめ