ThreadLocal详解

一、什么是ThreadLocal

1、什么是ThreadLocal&为什么用ThreadLocal

ThreadLocal,即线程本地变量,在类定义中的注释如此写This class provides thread-local variables。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。属于空间换时间的解决线程安全问题的方案。

2、ThreadLocal的使用场景

  • 在某些项目中,日志需要存储用户的信息,因此在切面中,可以使用ThreadLocal存储用户信息,或者使用ThreadLocal存储各个接口的返回结果,在切面中统一处理
  • 在格式化日期的时候,用到SimpleDateFormat,需要使用Thread Local来保证线程安全,如下
public class test {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    System.out.println(sdf.parse("2023-03-17 10:34:30"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

ThreadLocal详解_第1张图片
出现了线程安全问题,接下来,我们使用ThreadLocal处理

public class test {
    private static final ThreadLocal sdf = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    System.out.println(sdf.get().parse("2023-03-17 10:34:30"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

ThreadLocal详解_第2张图片
这是因为SimpleDateFormat是一个线程不安全类,在多线程情况下会出现问题,而通过ThreadLocal处理后,变成了每个线程私有的一个类,因此成功运行。

二、ThreadLocal的原理

1、关键代码分析

先来看下Thread类中与ThreadLocal有关的代码和ThreadLocal的关键代码

class Thread{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

从代码中可以看到,ThreadLocal.ThreadLocalMap是Thread的一个属性,也就是说,一个线程持有一个ThreadLocal.ThreadLocalMap对象。

class ThreadLocal{
	//ThreadLocal的set方法
	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    //ThreadLocal的get方法
    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();
    }
    //ThreadLocalMap类
    static class ThreadLocalMap{
         private Entry[] table;
         static class Entry extends WeakReference>{
             Object value;
             Entry(ThreadLocalk, Object v){
                 super(k);
                 value = v;
             }
         }
    }
    //获取map方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

ThreadLocalMap是ThreadLocal类的静态内部类,ThreadLocalMap内部维护了Entry数组,每个Entry是一个完整的key-value对象,其中key为ThreadLocal本身。接着看ThreadLocal的get/set方法,都是先获取当前线程对象t,然后通过getMap方法返回当前线程对象t的threadLocals对象,也就是线程持有的ThreadLocalMap对象,然后将当前线程对象作为key传入,从ThreadLocalMap对象中获取值或者设置值。综上,我们可以做出一个结构图。

2、ThreadLocal和Thread结构图

ThreadLocal详解_第3张图片

3、ThreadLocal原理概述

综合上面的代码分析和结构图,我们可以得出ThreadLocal的基本原理

Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。

三、引用问题&内存泄漏

1、Java的引用类型

  • 强引用:我们常用的new对象就是强引用,Object obj = new Object();这种引用对象在内存不足时,jvm宁愿抛出oom错误也不会被回收
  • 软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:无论内存是否足够,只要jvm开始进行垃圾回收,那些被弱引用关联的对象都会被回收
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
/**
 * 测试引用类型,jvm参数设置为-Xms10M -Xmx10M
 */
public class test {
    private static Listlist = new ArrayList<>();
    public static void main(String[] args) {
        testWeakReference();
    }

    /**
     * 强引用
     * 直接报错OOM
     */
    public static void testStrongReference(){
        byte[] bytes = new byte[1024*1024*11];
    }

    /**
     * 软引用,当发生gc时,内存不存会回收对象
     * null
     * null
     * null
     * null
     * [B@7eda2dbb
     */
    public static void testSoftReference(){
        for (int i = 0; i < 5; i++) {
            byte[] bytes = new byte[1024*1024*5];
            SoftReference softReference = new SoftReference<>(bytes);
            list.add(softReference);
        }
        System.gc();
        for (int i = 0; i < list.size(); i++) {
            Object o = ((SoftReference)list.get(i)).get();
            System.out.println(o);
        }
    }

    /**
     * 弱引用,发生gc时,无论内存是否足够,都回收对象
     * null
     * null
     * null
     * null
     * null
     */
    public static void testWeakReference(){
        for (int i = 0; i < 5; i++) {
            byte[] bytes = new byte[1024*1024*5];
            WeakReference weakReference = new WeakReference<>(bytes);
            list.add(weakReference);
        }
        System.gc();
        for (int i = 0; i < list.size(); i++) {
            Object o = ((WeakReference)list.get(i)).get();
            System.out.println(o);
        }
    }
}
 
  

2、为什么ThreadLocalMap的Entry对象的key用的是弱引用

如下图所示,Entry对象的key继承了弱引用的ThreadLocal
ThreadLocal详解_第4张图片
我们先看下这个ThreadLocal的引用图
ThreadLocal详解_第5张图片
我们先假设用的是强引用,只要我们的线程一直存活(或者使用了线程池),那么,无论ThreadLocal变量的引用存在与否,ThreadLocal对象都会被entry对象引用,那么就造成了即使ThreadLocal变量的引用不存在了,这个ThreadLocal对象也不会被回收,造成内存泄漏;而设置成弱引用,那么发生gc,检查到ThreadLocal对象只存在弱引用,就会被回收。

3、内存泄漏

先来看一段代码及其两种演示结果

/**
 * jvm设置为最大20m
 * 如果不调用remove方法,运行过程中出现oom错误
 * 执行remove方法,不会出现oom错误
 */
public class test {
    static class zyObject{
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }

    private static ThreadLocal zyObjectThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
        for (int i = 0; i < 10; i++) {
            int a = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第"+a+"个线程");
                    zyObject zyObject = new zyObject();
                    zyObjectThreadLocal.set(zyObject);
                    zyObject = null;//将对象设置为 null,表示此对象不在使用了
//                    zyObjectThreadLocal.remove();
                }
            });
            Thread.sleep(1000);
        }
    }
}

ThreadLocal详解_第6张图片
ThreadLocal详解_第7张图片

上面说到,Entry对象的key使用弱引用的ThreadLocal对象,发生GC时会被回收,那么,还存在什么内存泄漏问题呢?这是因为ThreadLocal回收的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。所以我们使用完ThreadLocal之后,要记得调用remove方法。实际上,ThreadLocal在设计的时候也考虑过这些问题,在set、get、remove方法中都有对key为null的移除。

四、如何使用ThreadLocal父子线程传值

我们知道,ThreadLocal作为本地线程变量,它的变量是私有的,那么如何进行线程间的传值呢?先来看下面这段代码

/**
 * 运行结果
 * null
 * 我是BBBBBBBBBBBBBBB
 */
public class test {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
        threadLocal.set("我是AAAAAAAAAAAAAAAAA");
        inheritableThreadLocal.set("我是BBBBBBBBBBBBBBB");
        new Thread(()->{
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        }).start();
    }
}

可以看到,InheritableThreadLocal实现了父子线程间的传值。我们看下Thread类的初始化代码

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) { 
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
    }

可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,数据从父线程那里来的。

总结

这篇文章主要从ThreadLocal的结构、原理、常见问题方面阐述了ThreadLocal的相关知识,并没有去讲解ThreadLocalMap的初始化、扩容、重新hash等知识,这些内容在日常使用中不算特别重要,如果需要了解,适当看一下源码及Map的原理即可。

你可能感兴趣的:(学习总结,jvm,spring,boot,spring)