ThreadLocal从入门到放弃

    技术2024-10-06  71

    1、ThreadLocal

    1.1 定义

    用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get或者set方法访问)时能保证各个线程的变量对应相对独立于其他线程内的变量。

    作用是:提供了线程内部的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

    1.2 示例

    需求:线程隔离

    在多线程并发的场景下,每个线程中变量都是相互隔离的

    线程A:设置变量1,获取变量1 线程B:设置变量2,获取变量2

    1.2.1 错误实现

    public class ThreadLockDemo { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public static void main(String[] args) { ThreadLockDemo demo = new ThreadLockDemo(); for (int i = 1; i <= 10; i++) { new Thread(new Runnable() { @Override public void run() { demo.setName(Thread.currentThread().getName() + "的数据"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--->" + demo.getName()); } }, "线程" + i).start(); } } }

    运行结果

    线程4--->线程10的数据 线程3--->线程10的数据 线程8--->线程10的数据 线程10--->线程10的数据 线程2--->线程10的数据 线程9--->线程10的数据 线程7--->线程10的数据 线程5--->线程10的数据 线程6--->线程10的数据 线程1--->线程10的数据

    1.2.2 使用 ThreadLocal 解决

    ThreadLocal 1、set() 将变量绑定到当前线程中 2、get() 获取当前线程绑定的变量

    public class ThreadLockDemo { ThreadLocal<String> threadLocal = new ThreadLocal<>(); private String name; // public String getName() { // return name; // } // // public void setName(String name) { // this.name = name; // } public String getName() { return threadLocal.get(); } public void setName(String name) { threadLocal.set(name); } public static void main(String[] args) { ThreadLockDemo demo = new ThreadLockDemo(); for (int i = 1; i <= 10; i++) { new Thread(new Runnable() { @Override public void run() { demo.setName(Thread.currentThread().getName() + "的数据"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--->" + demo.getName()); } }, "线程" + i).start(); } } }

    运行结果

    线程8--->线程8的数据 线程1--->线程1的数据 线程5--->线程5的数据 线程6--->线程6的数据 线程7--->线程7的数据 线程10--->线程10的数据 线程3--->线程3的数据 线程9--->线程9的数据 线程2--->线程2的数据 线程4--->线程4的数据

    1.3 使用 synchronize 来处理

    1.3.1 使用synchronize 加锁来实现

    使用 synchronize 加锁也能解决这个问题

    public class ThreadLockDemo { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public static void main(String[] args) { ThreadLockDemo demo = new ThreadLockDemo(); for (int i = 1; i <= 10; i++) { new Thread(new Runnable() { @Override public void run() { synchronized (ThreadLockDemo.class){ demo.setName(Thread.currentThread().getName() + "的数据"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--->" + demo.getName()); } } }, "线程" + i).start(); } } }

    运行也没有问题,但是加锁后,各个线程需要排队进入运行,性能会降低,造成该操作不是并发运行。

    1.3.2 ThreadLocal 与 synchronize 的区别

    ​ 都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。

    synchronizeThreadLocal原理同步机制采用 以时间换空间的方式,只提供了一份变量,让不容的线程排队访问ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据互相隔离

    2 在数据库连接案例中使用到了 ThreadLocal

    传递数据:保存每个线程绑定的数据,在需要的地方直接获取,避免参数直接传递带来的代码耦合问题线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

    3 ThreadLocal 的内部结构

    JDK8 中 ThreadLocal 的设计是:

    每一个Thread线程内部都有一个Map(ThreadLocalMap)Map 里边存储 ThreadLocal 对象(key)和线程的变量副本(value)Thread 内部的Map是由ThreadLocal维护的,由ThreadLocal负责向 map 获取和设置线程的变量值对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 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(); } /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ 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; } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }

    4 为什么要使用弱引用

    无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免出现内存泄漏。

    要避免内存泄漏有两种方式:

    使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry使用完 ThreadLocal,当前Thread也随之运行结束

    第二种方式是不好控制,特别是使用线程池的时候,线程结束不会销毁的。

    也就说,在使用完后,记得调用 remove,无论是强引用还是弱引用都不会有问题,那为什么要使用弱引用呢?

    在 ThreadLocalMap 中的 set/getEntry 的方法中,会对 key 为 null(即 ThreadLocal 为 null)进行判断,如果为 null 的话,那么会对 value 设置为 null。

    即在使用完 ThreadLocal ,CurrentThread 依然运行的情况下,就算忘记调用 remove 方法,弱引用比强引用多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set/get/remove中的任意方法时都会被清除,从而避免内存泄漏。

    总结:ThreadLocal内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除掉对应的 key 就会导致内存泄漏。

    Processed: 0.013, SQL: 9