ThreadLocal
目录
1、不合理的使用
很多人只将ThreadLocal作为一种用于“方便传参”的工具,但我觉得这或许并不是ThreadLocal设计的目的,它本身是为线程安全和某些特定场景的问题而设计的。
此时我们先不急于去证明这个想法,我们先去懂得threadLocal其工作原理,
2、源码简解
- get方法:读取实例时,线程首先通过
getMap(t)
方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射。获取到 ThreadLocalMap 后,通过map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象,如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。public T get() { Thread t = Thread.currentThread();//获取当前正在运行的线程 ThreadLocalMap map = getMap(t);//获得一张ThreadLocalmap(这个map在后面讲解) if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//获得与这个线程中ThreadLocal相关联的实例 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();//如果并无实例,则初始化。 }
- setInitialValue方法:该方法为 private 方法,无法被重载。首先,通过initialValue()方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。
这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。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; }
- set方法:除了通过
initialValue()
方法设置实例的初始值,还可通过 set 方法设置线程内实例的值,如下所示。该方法先获取该线程的 ThreadLocalMap 对象,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。另外,如果获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。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中的Entry是继承WeakReference的,其中ThreadLocal是以弱引用形式存在Entry中,如果ThreadLocal在外部没有被强引用,那么垃圾回收的时候就会被回收掉,又因为Entry中的value是强引用,就会出现内存泄漏。虽然ThreadLocal源码中的会对这种情况进行了处理,但还是建议不需要用TreadLocal的时候,手动调remove方法。
static class ThreadLocalMap { private static final int INITIAL_CAPACITY = 16;//初始数组大小 private Entry[] table;//每个可以拥有多个ThreadLocal private int size = 0; private int threshold;//扩容阀值 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 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); } private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); 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) { //循环利用key过期的Entry replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } }
3、ThreadLocal用法:
package com.mec.mfct.test;
public class TestThreadLocal {
public static void main(String[] args) throws InterruptedException {
int threads = 3;
InnerClass innerClass = new InnerClass();
for(int i = 1; i <= threads; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 4; j++) {
innerClass.add(String.valueOf(j));
innerClass.print();
}
innerClass.set("hello world");
}
}).start();
}
}
private static class InnerClass {
public void add(String newStr) {
StringBuilder str = StringBuilderFactory.local.get();
StringBuilderFactory.local.set(str.append(newStr));
}
public void print() {
System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
StringBuilderFactory.local.hashCode(),
StringBuilderFactory.local.get().hashCode(),
StringBuilderFactory.local.get().toString());
}
public void set(String words) {
StringBuilderFactory.local.set(new StringBuilder(words));
System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
StringBuilderFactory.local.hashCode(),
StringBuilderFactory.local.get().hashCode(),
StringBuilderFactory.local.get().toString());
}
}
private static class StringBuilderFactory {
private static ThreadLocal<StringBuilder> local = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
}
}
运行结果如下:
- 从输出可见,每个线程通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例
- 每个线程所访问到的是同一个 ThreadLocal 变量
- 由代码可见,虽然从代码上都是对 StringBuidlerFactory 类的静态 counter 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
- 使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换
所以从上述例子中可以看出,ThreadLocal并非是方便传参。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被
private static
修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
如果单看其中某一点,替代方法很多。比如可通过在线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间的共享。但如果要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。
4、总结:
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。
- 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
- ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题。
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景。
- 在使用Threadlocal后及时remove,避免内存泄漏。