ThreadLocal使用,线程安全,数据传递,弱引用及内存泄漏
文章目录
概述
是什么?
-
ThreadLocal
提供线程的局部变量。填充在ThreadLocal
中的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal
为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 -
通俗来说的话,
ThreadLocal
提供线程的局部变量,可以把ThreadLocal
理解为一个容器,每个线程可以绑定多个ThreadLocal
对象,每个ThreadLocal
对象绑定的数据对于不同线程来说是数据隔离的。
有啥用?
- 解决线程安全问题:
- 它使用另外一种思路解决线程安全问题,线程不安全在于多线程环境下对于共享数据的访问,导致结果和预期结果不一致,一般会使用同步机制加锁来解决对于共享变量的访问,而
ThreadLocal
为每一个线程提供独立的变量副本,线程不存在对共享数据的访问;从另外一种思路解决了多线程环境对于共享数据的争取导致的线程不安全。
- 它使用另外一种思路解决线程安全问题,线程不安全在于多线程环境下对于共享数据的访问,导致结果和预期结果不一致,一般会使用同步机制加锁来解决对于共享变量的访问,而
- 数据传递问题:
- 每一个ThreadLocal的都绑定了当前线程,只需要在当前线程中设置过ThreadLocal中变量的值,后续在当前线程中任意位置,就可以通过ThreadLocal访问变量的值,作为一个上下文对象使用,从而避免代码的耦合度。
怎么用?
-
在日常使用过程中,一般我们只需要ThreadLocal向外暴露的基础api即可。
-
ThreadLocal.initialValue()
通过子类重写来设置默认值,或者通过Supplier
供给型函数式接口withInitial
方法提供默认值。为ThreadLocal绑定的变量设置默认值,能够有效的避免空指针,以及不必要的判空逻辑。 -
void ThreadLocal.set(T)
将此线程局部变量的当前线程副本设置为指定值。可以简单理解为给当前线程ThreadLocal存储的数据赋值。 -
T ThreadLocal.get()
返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,则首先将其初始化为调用initialValue方法返回的值。简单理解为获取当前线程中ThreadLocal存储的值。 -
void ThreadLocal.remove()
删除此线程局部变量的当前线程值。如果这个线程局部变量随后被当前线程读取,它的值将通过调用它的initialValue方法重新初始化。简单理解为删除当前线程中ThreadLocal存储的值。 -
public class QuickStartDemo { public static void main(String[] args) { new Thread(() -> { // 存储数据:根据业务来决定要你存储的数据,可能是一个入参,可能是当前对象的userId Integer data = Integer.valueOf("1"); Context.set(data); // 经过多层调用处理之后,现在可能已经不在之前的类中,你想要获取数据,只需要通过Context作为key,就能拿到当前线程你存储的数据 Integer result = Context.get(); System.out.println(Thread.currentThread().getName() + " Context.get() = " + result); // 处理完后,及时清理数据 Context.clear(); }).start(); // 因为主线程没有设置过值,所有获取的数据为默认值 System.out.println(Thread.currentThread().getName() + " Context.get() = " + Context.get()); } } class Context { /** * 创建ThreadLocal,并设置默认值(懒加载),一般作为一个静态变量存储 */ private final static ThreadLocal<Integer> CONTEXT = ThreadLocal.withInitial(() -> 0); public static Integer get() { return CONTEXT.get(); } public static void set(Integer data) { CONTEXT.set(data); } public static void clear() { CONTEXT.remove(); } }
-
运行结果
-
main Context.get() = 0 Thread-0 Context.get() = 1
类图
-
-
每个Thread中维护了一个ThreadLocalMap对象,他是ThreadLocal中的一个静态内部类,其中定义了真正操作数据的api,供上层的ThreadLocal调用。当前的ThreadLocal对象作为key,存储的数据作为value。
-
ThreadLocalMap中维护了一个Entry数组,用来真正存放数据,Entry继承了WeakReference(弱引入:只要发生gc,不管内存是否足够,都会回收引用的对象),在Entry中,ThreadLocal对象就是通过弱引用引用的。
-
SupplierdThreadLocal用来以更加优雅的方式初始化ThredLocal的默认值。它实现了
initialValue()
,通过Java的供给型函数式接口Supplier
提供延迟赋值的效果。通过ThreadLocal.withInitial初始化。
结构图
-
-
每一个线程都会维护一个ThreadLocalMap对象,用来存储当前线程的线程局部变量,以ThreadLocal为key,存储的数据作为value。这也就是为什么同一个ThreadLocal在不同线程下获取的数据不同,因为他们存储的Map不一样。
-
TreadLocalMap并不继承Java的Map接口,而是独立实现的。它底层使用Entry数组来存储数据。
-
代码案例
线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。 —— 《Java并发编程实践》
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。
如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。 ——《深入理解Java虚拟机》
-
对于下面这段代码,就是非线程安全的,共享数据
SimpleDateFormat
对象被可能被多个线程同时访问,由于内部的引用了Calendar
对象;它是有个有状态对象,有很多的成员变量记录当前状态,且向外提供的api并未使用同步机制,导致在多线程环境下的不安全。 -
public class ThreadSafeIssueDemo01 { public static void main(String[] args) { // 多个线程共享的数据 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ExecutorService executorService = Executors.newFixedThreadPool(5, Thread::new); for (int i = 0; i < 10; i++) { executorService.submit(() -> { Date date = null; try { date = simpleDateFormat.parse("2022-9-14 11:11:11"); } catch (Exception e) { System.out.println("发生错误:" + Thread.currentThread().getName()); e.printStackTrace(); } if (date != null) { System.out.println(simpleDateFormat.format(date)); } }); } executorService.shutdown(); } }
-
执行结果
-
发生错误:Thread-4 2220-09-14 11:11:11 发生错误:Thread-0 发生错误:Thread-1 发生错误:Thread-2 2022-09-14 11:11:11 2022-09-14 11:11:11 2022-09-14 11:11:11 2022-09-14 11:11:11 2022-09-14 11:11:11 java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.lang.Double.parseDouble(Double.java:538) at java.text.DigitList.getDouble(DigitList.java:169) at java.text.DecimalFormat.parse(DecimalFormat.java:2056) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364)
-
在转换为date对象时,可能是多个线程同时发生的,根据操作系统的调度以及线程的cpu时间片决定,例如,线程1将内部的一个状态a改为了1,线程1的由于cpu时间片用完,线程进入就绪状态,重新抢夺时间片;这时线程2抢夺到了时间片,线程进入运行状态,将状态a改为2,cpu时间片用完;这时线程1又抢夺到了线程资源,由于创建的对象是在堆内存中,它的数据是线程共享的,这时线程1读到的状态a就是2,出现了线程不安全的情况。
-
解决线程安全问题
-
加锁:线程同步执行
-
public class ThreadSafeIssueDemo02 { public static void main(String[] args) { // 多个线程共享的数据 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ExecutorService executorService = Executors.newFixedThreadPool(5, Thread::new); for (int i = 0; i < 10; i++) { executorService.submit(() -> { Date date = null; try { synchronized (simpleDateFormat) { date = simpleDateFormat.parse("2022-9-14 11:11:11"); } } catch (Exception e) { System.out.println("发生错误:" + Thread.currentThread().getName()); e.printStackTrace(); } if (date != null) { System.out.println(simpleDateFormat.format(date)); } }); } executorService.shutdown(); } }
-
-
不使用共享对象
-
每次都重新创建一个对象
-
public class ThreadSafeIssueDemo03 { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5, Thread::new); for (int i = 0; i < 10; i++) { executorService.submit(() -> { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = null; try { date = simpleDateFormat.parse("2022-9-14 11:11:11"); } catch (Exception e) { System.out.println("发生错误:" + Thread.currentThread().getName()); e.printStackTrace(); } if (date != null) { System.out.println(simpleDateFormat.format(date)); } }); } executorService.shutdown(); } }
-
-
使用ThreadLocal,本质上是和上面一样的,每次都重新创建了对象,不同的地方在于使用ThreadLocal,存储的对象是可以在当前线程复用的,而每次创建对象,在方法栈pop之后,就会被回收,无法复用。
-
public class ThreadSafeIssueDemo04 { private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5, Thread::new); for (int i = 0; i < 10; i++) { executorService.submit(() -> { Date date = null; try { date =simpleDateFormatThreadLocal.get().parse("2022-9-14 11:11:11"); } catch (Exception e) { System.out.println("发生错误:" + Thread.currentThread().getName()); e.printStackTrace(); } if (date != null) { System.out.println(simpleDateFormatThreadLocal.get().format(date)); } simpleDateFormatThreadLocal.remove(); }); } executorService.shutdown(); } }
-
-
-
数据传递
-
可以把mian函数理解为controller层的一个api入口。for循环10次,代表是同时处理的10个请求,每个请求一个线程。通过ThreadLocal在service层存储了数据,在dao层拿出来使用,使用完之后清理,每个线程只会拿到当前线程设置的值。
-
public class DataTransmitDemo { private static final ExecutorService executorService = Executors.newFixedThreadPool(10, Thread::new); public static void main(String[] args) { // 提交任务 for (int i = 0; i < 10; i++) { executorService.submit(() -> { // 处理service层逻辑 new Service().handler(); // 及时清理使用完的线程变量 HeaderContext.clear(); }); } executorService.shutdown(); } } class Service { public void handler() { // 获取当前线程名【放入上下文对象】 char c = Thread.currentThread().getName().charAt(7); HeaderContext.setContext((int) c - 48); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 处理dao层业务逻辑 new Dao().handler(); } } class Dao { public void handler() { // 业务逻辑... // 获取上下文数据 System.out.println(Thread.currentThread().getName() + " --> HeaderContext.getContext() = " + HeaderContext.getContext()); } } class HeaderContext { private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0); public static Integer getContext() { return threadLocal.get(); } public static void setContext(Integer data) { threadLocal.set(data); } public static void clear() { threadLocal.remove(); } }
-
Api源码
-
set方法简单来说就是先获取当前线程维护的ThreadLocalMap对象,没有就创建map,有就将当前的ThreadLocal对象作为key存储进ThreadLocalMap中。
-
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取线程t中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) // 调用ThreadLocalMap的set方法存储数据 map.set(this, value); else // map为空,创建ThreadLocalMap createMap(t, value); } ThreadLocalMap getMap(Thread t) { // 获取线程t中维护的ThreadLocalMap对象 return t.threadLocals; } void createMap(Thread t, T firstValue) { // 构造方法创建map,赋值给当前线程维护的ThreadLocalMap t.threadLocals = new ThreadLocalMap(this, firstValue); } private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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; } } // 创建Entry对象,存储数据 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
-
get方法也是获取当前线程维护的ThreadLocalMap,map不为空,直接获取Entry,map为空,获取设置的默认值,并且创建map。
-
public T get() { // 获取当前线程维护的ThreadLocalMap Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { // ThreadLocalMap.getEntry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // map为空或者Entry为空,获取默认值 return setInitialValue(); } 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; }
弱引用与内存泄露
-
ThreadLocal的key和value都有可能造成内存泄露,ThreadLocal造成内存泄露的原因在于内部的Entry对象不能通过引用访问到,但是还没有被垃圾回收。
-
在说为什么出现内存泄露之前,需要一点前置知识,简单说一下Java中的引用关系
强软弱虚
。-
强引用:一般手动创建出的的都是强引用。在发生gc时,不过内存够不够,都不会回收该对象,除非是没有引用可达。
-
Object objectRef = new Object();
-
-
软引用:在发生gc时,内存足够的时候,不回收对象,内存不足会回收。
-
SoftReference<Object> reference = new SoftReference<MyObject>(new Object()); System.out.println("reference.get() = " + reference.get());
-
-
弱引用:在发生gc时,不管内存是否足够,都回收内部的引用对象
-
WeakReference<Object> weakReference = new WeakReference<>(new Object()); System.out.println("weakReference.get() = " + weakReference.get());
-
-
虚引用:get方法永远获取到的是null,跟踪对象被垃圾回收器回收的活动,被回收时会放入队列。
-
Object Object = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<MyObject> phantomReference = new PhantomReference<>(Object, queue);
-
-
后面三个的引用,显式的写出来的是强引用,get出来的对象才是说的各种引用。
-
产生原因
-
首先是在栈空间,ThreadLocal的强引用,引用了堆中的ThreadLoca对象;当前线程引用ThreadLocalMap。Entry的key弱引用ThreadLocalMap。
-
-
当线程还在持续运行,但是实际任务已经执行结束,同时没有手动删除Entry时,就可以发生内存泄露。有一条强引用链存在,导致Entry对象不能被垃圾回收。
为什么使用弱引用
-
当业务代码中使用玩ThreadLocal之后,ThreadLocalRef就会被回收,当这条唯一的强引用断开之后,发生gc时,由于Entry中的key只是弱引用指向ThreadLocal,不会影响ThreadLocal的回收,他会被正常回收。也就是ThreadLocal的key会被正常回收。
-
但是由于没有手动删除Entry对象,当前线程仍然有强引用链路存在,导致Entry的value无法被回收,从而可能导致内存泄露。
-
既然这样,为什么还需要设置为弱引用呢,因为其实你没有手动调用remove方法,ThreadLocal在下一次调用set,get方法时,都会清理key为null的数据。
-
及时这样,也最好还是调用一下remove方法。因为下一次调用,不知道什么时候,能够避免就避免。
注意事项
-
记得设置默认值,有效避免空指针以及不必要空指针。(记得使用java8的函数式接口,优雅)
-
使用完ThreadLocal之后,记得调用remove方法,避免造成内存泄露。同时使用线程池时,如果不手动remove,因为线程会复用,所有会造成ThreadLocal获取到错误数据。
-
public class Notes01 { public static void main(String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { int finalI = i; executorService.submit(() -> { System.out.println("threadLocal = " + threadLocal.get()); threadLocal.set(finalI); // 记得remove! //threadLocal.remove(); }); } executorService.shutdown(); } }
-
执行结果:
-
threadLocal = null threadLocal = 0 threadLocal = 1 threadLocal = 2 threadLocal = 3 threadLocal = 4 threadLocal = 5 threadLocal = 6 threadLocal = 7 threadLocal = 8
-
-
-
请使用remove方法,而不是使用set(null)。调用remove方法,是直接删除了Entry对象,而set(null),只是把value置空,key还有数据,可能就是压死骆驼的最后一根稻草。