ThreadLocal 到底是什么?它解决了什么问题?,kalilinux 渗透教程视频
}, "thread - " + i).start();}countDownLatch.await();}
private static class MyUtil {
public static void add(String newStr) {StringBuilder str = StringBuilderUtil.stringBuilderThreadLocal.get();StringBuilderUtil.stringBuilderThreadLocal.set(str.append(newStr));}
public static void print() {System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",Thread.currentThread().getName(),StringBuilderUtil.stringBuilderThreadLocal.hashCode(),StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),StringBuilderUtil.stringBuilderThreadLocal.get().toString());}
public static void set(String words) {StringBuilderUtil.stringBuilderThreadLocal.set(new StringBuilder(words));System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",Thread.currentThread().getName(),StringBuilderUtil.stringBuilderThreadLocal.hashCode(),StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),StringBuilderUtil.stringBuilderThreadLocal.get().toString());}}
private static class StringBuilderUtil {// ThreadLocal 变量通常被 private static 修饰 private static ThreadLocal<StringBuilder> stringBuilderThreadLocal = ThreadLocal.withInitial(() -> new StringBuilder());}
}
实例分析
ThreadLocal 本身支持范型,比如该例使用了 StringBuilder 类型的 ThreadLocal 变量。可通过 ThreadLocal 的 get() 方法读取 StringBuidler 实例,也可通过 set(T t) 方法设置 StringBuilder。
tips:CountDownLatch 类位于 java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个场景:任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种功能了。下次,我们可以单独聊聊这一个功能。
点击运行,控制台输出结果
我们可以发现:
每个线程访问的是同一个 ThreadLocal 变量,而通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例;
虽然从代码上都是对 StringBuilderUtil 类的静态 stringBuilderThreadLocal 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换
ThreadLocal 原理
方案一
我们大胆猜想一下,既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案是 ThreadLocal 维护一个 Map,Key 是当前线程,Value 是 ThreadLocal 在当前线程内的实例。这样,线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示
这个方案可以满足上文提到的每个线程内部都有一个 ThreadLocal 实例备份的要求。每个新线程访问该 ThreadLocal 时,都会向 Map 中添加一个新的映射,而当每个线程结束时再清除该线程对应的映射。But,这样就存在两个问题:
开启线程与结束线程时我们都需要及时更新 Map,因此必需保证 Map 的线程安全。
当线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。
线程安全问题是 JDK 未采用该方案的一个主要原因。
方案二
上面这个方案,存在多线程访问同一个 Map 时可能会出现的同步问题。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,就不存在这个问题。该方案如下图所示。
该方案虽然没有锁的问题,但是由于每个线程在访问 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),就有可能会造成内存泄漏的问题。我们一起来看一下 Jdk8 是如何解决这个问题的。
ThreadLocal 在 JDK 8 中的实现
ThreadLocalMap 与内存泄漏
在该方案中,Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个?Entry?都是一个对?Key?的弱引用,这一点我们可以从super(k)
可看出。另外,每个 Entry 中都包含了一个对?Value?的强引用。
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;
Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
之所以使用弱引用,是因为当没有强引用指向 ThreadLocal 变量时,这个变量就可以被回收,就避免 ThreadLocal 因为不能被回收而造成的内存泄漏的问题。
但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
**注意:**Entry 是对 ThreadLocal 类型的弱引用,并不是具体实例的弱引用,因此还存在具体实例相关的内存泄漏的问题。
读取实例
我们来看一下 ThreadLocal 获取实例的方法
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(t)
方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射,这一点与上文分析一致。
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
获取到 ThreadLocalMap 后,通过map.getEntry(this)
方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象。
如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()
方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。
设置初始值
设置初始值方法如下
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
该方法为 private 方法,无法被重载。
首先,通过initialValue()
方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。
然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象与对应实例
初始值的映射添加进该线程的 ThreadLocalMap 中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。
这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。
设置实例
除了通过initialValue()
方法设置实例的初始值,还可通过 set 方法设置线程内实例的值,如下所示。
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}
该方法先获取该线程的 ThreadLocalMap 对象,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。另外,如果获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。
防止内存泄漏
对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。
针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。
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) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
案例
对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session 实例,并将该实例在多个方法间传递,如下所示。
public class SessionHandler {
@Datapublic static class Session {private String id;private String user;private String status;}
public Session createSession() {return new Session();}
public String getUser(Session session) {return session.getUser();}
public String getStatus(Session session) {return session.getStatus();}
public void setStatus(Session session, String status) {session.setStatus(status);}
public static void main(String[] args) {new Thread(() -> {SessionHandler handler = new SessionHandler();Session session = handler.createSession();handler.getStatus(session);handler.getUser(session);handler.setStatus(session, "close");handler.getStatus(session);}).start();}}
评论