理解ThreadLocal:Java多线程编程的关键技术

411 阅读4分钟

ThreadLocal概述

  ThreadLocal是Java中一个非常重要的多线程编程工具,它主要用于在多线程环境中实现线程间数据的隔离。

  在多线程编程中,线程之间可能会共享某些变量或对象,如果没有正确地控制这些共享资源,就会出现线程安全问题,比如数据竞争、并发访问等问题。而ThreadLocal提供了一种简单、方便的方式来实现线程间数据的隔离,从而避免线程安全问题的出现。

  ThreadLocal通过在每个线程中创建一个独立的副本来实现数据的隔离,每个线程都可以访问自己的副本,但是无法访问其他线程的副本。这样,线程之间就可以独立地操作数据,而不会互相干扰,从而保证了线程安全性。

ThreadLocal使用

public class Demo {

    static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();


    public static class MyThread extends Thread{

        int i ;

        public MyThread(int i){
            this.i = i;

        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            threadLocal.set(i);
            if(i == 1){
                threadLocal2.set("demo");
            }
            System.out.println(name+":"+threadLocal.get());
            System.out.println(name+":"+threadLocal2.get());
        }
    }


    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new MyThread(i).start();
        }
    }
}

输出结果:

image.png

总结

  0 1 2 三个线程设置的变量和获取的变量没有差错说明ThreadLocal确实做到了线程间数据的隔离。

实现类似ThreadLocal的功能

  根据上述的例子我们来仿照一下他的功能,想实现线程隔离的话我们可以使用map,当前线程对象就作为key,这样子取数据的时候根据key每个线程对象就只能拿到自己的value。

public class MyThreadLocal<T> {

    private Map<Thread,T> threadTMap = new HashMap<>();

    public synchronized T get(){
        return  threadTMap.get(Thread.currentThread());
    }

    public synchronized void set(T t){
        threadTMap.put(Thread.currentThread(),t);
    }

}

这种实现方式是可以做到线程间数据隔离的但是有相当大的性能问题。

image.png

ThreadLocal实现原理

ThreadLocal.set()源码

image.png   可以看到他这个set中有一个类似map的东西叫做ThreadLocalMap

image.png   可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了两个信息,一个是 ThreadLocal<?>类型,一个是 Object 类型的值。getEntry 方法则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal对应的值。

  另外ThreadLocalMap是作为一Thread的变量的,也就是每个Thread都会有自己的ThreadLocalMap。

image.png 按照ThreadLocal的数据形式我们图像化一下刚刚上述的例子

ThreadLocal.png

内存泄露分析

java中的引用

  • 强引用:就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
  • 软引用:是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,提供了 SoftReference 类来实现软引用。
  • 弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了PhantomReference 类来实现虚引用。

内存泄漏现象

  当使用ThreadLocal时,每个线程都会创建一个ThreadLocalMap对象,该对象用于存储当前线程的所有ThreadLocal变量。在线程池中,如果线程不会被销毁,那么这个ThreadLocalMap对象就会一直存在,并且其中的ThreadLocal变量也不会被清除,这就会导致内存泄漏问题。

  根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

  因此使用了ThreadLocal后,引用链如图所示:

ThreadLocal引用链.png

  如图我们的数据其实是保存在ThreadLocalMap当中的,ThreadLocalMap是Thread中的一份变量那么他的生命周期也就和Thread一样,在线程结束之前没有显式地删除或清除与 ThreadLocal 相关联的数据,那么这些数据将会一直存在于内存中,而不会被垃圾回收机制回收。但是由于使用了线程池,这个线程池中的线程会一直存在直到JVM退出,从而导致了内存泄漏。