ThreadLocal 是什么?
ThreadLocal 的 JavaDoc 上的描述是这样的...
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
这个类提供了线程本地变量。这个变量和正常的变量不同。通过 get & set 方法,每个线程可以获取到自己独立的变量。这个变量实例通常是私有且静态的,可以存储与线程相关的信息,如员工 id、事务 id 等。
ThreadLocal 能解决什么问题?
线程并发问题。因为 ThreadLocal 解决了变量共享的问题。ThreadLocal 还有一个隐含的好处,那就是不需要将参数在方法中进行传递,可以直接从线程中获取。
举个栗子:
- SimpleDateFormat 是线程不安全的,所以很多项目中在使用 SimpleDateFormat 对象的时候都是将其放入 ThreadLocal 中。
- Shiro 中的 Subject 就是采用 ThreadLocal 实现的~SecurityUtils.getSubject();
- Spring 中事务信息就是通过 ThreadLocal 进行传递的
@Test public void testSimpleDateFormat() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for (int i = 0; i < 10; ++i) { new Thread(() -> { try { System.out.println(sdf.parse("2017-12-13 15:17:27")); } catch (ParseException e) { e.printStackTrace(); } }).start(); } }
上述代码运行,就会大概率出现 java.lang.NumberFormatException
(如果运气好没出现,可以尝试将线程数量调大一点儿)
SimpleDateFormat 之所以是非线程安全的是因为其内部 Calendar 是线程不安全的。归根结底是因为 Calendar 存放日期数据的变量。
ThreadLocal 是怎么设计的?
ThreadLocal 的类结构图
ThreadLocal 类主要是用了一个内部 ThreadLocalMap,这个 map 中存储的键值对是 Entry<ThreadLocal,Object>,Entry 类中只有 value 成员变量,key 是 ThreadLocal 对象,Entry 继承了 WeakReference
。
为什么要使用弱引用呢,起初的设计估计是为了让 GC 自动去清理已经挂掉的线程的相关 value。但是这有个问题,我们待会儿分析。
ThreadLocal 本身并没有持有这个 Map 对象,而是让 Thread 对象持有 Map 对象,大家可以想想这个是为什么。
ThreadLocal & ThreadLocalMap & Thread & Entry 的关系
- Thread 只能拥有一个 ThreadLocalMap 对象。
- 一个 ThreadLocalMap 对象存储多个 Entry 对象。
- Entry 对象的 key 弱引用指向一个 ThreadLocal 对象。
- 一个 ThreadLocal 对象可以被多个线程共享。
- ThreadLocal 对象不持有 value 对象,value 由 Entry 对象持有。
ThreadLocal 的灵魂
- set() 如果不设置值,那么容易引起脏数据问题。(比如上次这个线程被线程池回收,但是没有调用 remove,下文我们会提到这个问题)
- get() 如果没有 get(),那么我们找不到使用 ThreadLocal 的意义...
- remove() remove 方法一个是保证,接下来使用的时候不会出现脏数据,另外就是保证弱引用的 Entry 的 value 能被 GC 回收,不然会出现内存泄漏...下面有代码演示
ThreadLocal 应该怎么用?
我们一般用 ThreadLocal 的时候都会在一个类中,声明一个静态对象,让类持有 ThreadLocal。
再调用其 set()、get()方法,来实现赋值和取值。具体可以看如下代码
@Test public void testThreadLocal() throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(Task::new).start(); } for (int i = 0; i < 2; i++) { Thread.sleep(2000); } } static class Task implements Runnable { @Override public void run() { setName(Thread.currentThread().getName()); try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { log.error(e.getMessage()); } printName(Thread.currentThread().getName()); NameThreadLocal.remove(); } /** * threadLocal保存一下线程相关名称 * * @param name */ static void setName(String name) { NameThreadLocal.setName(name); } /** * 打印一下 取出ThreadLocal的名称,看是否和当前线程一致 * * @param threadName */ static void printName(String threadName) { log.info(threadName + "======" + NameThreadLocal.getName()); } } /** * 此类持有ThreadLocal静态对象 * 此处的Name可以是登录状态也可以是分布式的请求的traceId等等 */ static class NameThreadLocal { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); static void setName(String name) { threadLocal.set(name); } static String getName() { return threadLocal.get(); } static void remove() { threadLocal.remove(); } } // 结果如下 //Thread-3======Thread-3 //Thread-8======Thread-8 //Thread-5======Thread-5 //Thread-11======Thread-11 //Thread-7======Thread-7 //Thread-9======Thread-9 //Thread-12======Thread-12 //Thread-10======Thread-10 //Thread-6======Thread-6 //Thread-4======Thread-4
ThreadLocal 使用时有什么坑?
ThreadLocal 的 value 不能放共享变量
假设有一个共享变量 Object。
线程 A 设置 Object 的成员变量 property 为 x,放入了 ThreadLocal。
线程 B 设置 Object 的同一个成员变量 property 为 y,放入了 ThreadLocal。
假设两个线程同时执行,我们无法保证线程 B 再 get 的时候拿到的 property 的值是 x。
我们对上面的 Task 类代码进行改造,如下
static class Task implements Runnable { // 一个共享变量 static StringBuilder sb = new StringBuilder("start"); static AtomicInteger errorNum = new AtomicInteger(0); @Override public void run() { for (int i = 0; i < 50; i++) { try { TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } sb.append(i); } setName(Thread.currentThread().getName() + sb.toString()); printName(Thread.currentThread().getName() + sb.toString()); } /** * threadLocal保存一下线程相关名称 * * @param name */ static void setName(String name) { NameThreadLocal.setName(name); } /** * 打印一下 取出ThreadLocal的名称,看是否和当前线程一致 * * @param threadName */ static void printName(String threadName) { if (!threadName.equals(NameThreadLocal.getName())) { errorNum.getAndIncrement(); log.error(errorNum.toString()); } } }
经过测试,errorNum 基本上都是大于 0。大家可以自己试一下~
ThreadLocal 遇上线程池
ThreadLocal 遇上线程池的问题,就是容易发生内存泄漏。
我们还是对 Task 类进行改造,再增加一个测试方法。
static class Task implements Runnable { // 一个共享变量 static AtomicInteger errorNum = new AtomicInteger(0); @Override public void run() { // 我们将前两个请求设置名称,后面的不设置 if (errorNum.getAndIncrement() > 2) { setName(Thread.currentThread().getName()); } printName(Thread.currentThread().getName()); } /** * threadLocal保存一下线程相关名称 * * @param name */ static void setName(String name) { NameThreadLocal.setName(name); } /** * 打印一下 取出ThreadLocal的名称,看是否和当前线程一致 * * @param threadName */ static void printName(String threadName) { log.info(threadName + "=======" + NameThreadLocal.getName()); } } // 增加一个测试方法 @Test public void testMemoryLeak() { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { executorService.submit(new Task()); } } // 结果如下 //pool-1-thread-2=======null //pool-1-thread-3=======null //pool-1-thread-1=======pool-1-thread-1 //pool-1-thread-2=======pool-1-thread-2 //pool-1-thread-1=======pool-1-thread-1 //pool-1-thread-3=======pool-1-thread-3 //pool-1-thread-2=======pool-1-thread-2 //pool-1-thread-1=======pool-1-thread-1 //pool-1-thread-3=======pool-1-thread-3
通过上面的结果我们看到,即使一个线程执行完成了任务,在没有主动清空 ThreadLocal 的时候,再用之前线程池中的线程还会带有之前的 ThreadLocal 对应的 value.因为线程并没有被回收,ThreadLocal 的键值对并不会被清空,所以也就解答了上文我们提到的问题。这种问题一旦出现,会比较隐蔽。
解决办法就是我们在使用完成之后,需要主动进行释放
ThreadLocal.remove()
Thread 创建了子线程,ThreadLocal 怎么办?
我们可以看到 Thread 类中有一个成员变量是用来处理此情况的。
/* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
我们可以直接使用 ThreadLocal 的子类**InheritableThreadLocal。但是继承等操作在线程池中的话,可能会因为动态的创建线程而变得非常混乱。所以不是很建议在线程池用 InheritableThreadLocal。**在看代码的时候,可以看一下此方法的调用。
java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
总结
ThreadLocal 虽然有一些问题,但是我们不能因噎废食,否认其优秀。希望大家在使用的时候,注意上面的坑。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于