ThreadLocal简单分析

72 阅读5分钟

ThreadLocal总结

初始化ThreadLocal对象,以Integer类型为例。使用ThreadLocal,每个线程都会有这个变量的副本,线程之间修改变量不会相互干扰。

ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);//初值0

从Thread类说起

Thread类内部有个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal的静态内部类。

当前线程使用saleVolume变量时:

saleVolume.set(1+saleVolume.get());

saleVolume.get()方法,ThreadLocal的方法,获取当前线程的saleVolume存储的值。

    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();
    }

1、获取当前线程

2、getMap(t),根据当前线程获取ThreadLocalMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    //t.threadLocals就是取的这个属性:ThreadLocal.ThreadLocalMap threadLocals = null;

3、如果map等于null(第一次取出来是null),走初始化逻辑,setInitialValue()

    private T setInitialValue() {
        T value = initialValue();//默认是null
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//根据线程取ThreadLocalMap,第一次取是null
        if (map != null)
        //如果不为null,以当前的ThreadLocal对象为key,这里是saleValue,value为值放入map
            map.set(this, value);
        else
            createMap(t, value);//map为空,先创建
        return value;
    }
    
    //创建map
    void createMap(Thread t, T firstValue) {
        //当前ThreadLocal对象为key,传进来的firstValue是null
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

这样每个线程自己内部的ThreadLocalMap的threadLocals属性就不为null了。

相当于是这样
ThreadLocal.ThreadLocalMap threadLocals = new ThreadLocalMap(saleVolume, null);

4、如果map不为null,根据当前ThreadLocal对象(saleValume)获取一个Entry

ThreadLocalMap.Entry e = map.getEntry(this);

注意这里的关系,ThreadLocal内部有个静态内部类:ThreadLocalMap,ThreadLocalMap里面又有个静态内部类:Entry。Thread类里面有个ThreadLocalMap类型的属性。

Entry继承了弱引用,这很重要!

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
​
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看到Entry的键就是ThreadLocal对象。

5、如果Entry对象不为空,返回Entry对象的值。

set方法与get方法相比较是类似的逻辑。

内存泄漏

TreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

为了避免这种情况, 在不再需要使用 ThreadLocal 变量时调用 remove 方法,以清除当前线程的副本。这样可以确保 ThreadLocalMap 中的引用和值都得到正确的清理,避免内存泄漏。

线程池的内存泄露问题

在线程池中发生内存泄漏可能会导致一系列严重的问题,因为线程池中的线程是可以重复使用的,内存泄漏会在多个任务之间传播和累积。以下是在线程池中发生内存泄漏可能引发的问题:

  1. 内存占用增加: 如果在线程池中的任务频繁地使用 ThreadLocal 变量,而没有适当地清理,每个任务都会在 ThreadLocalMap 中留下副本。随着任务的执行,这些副本会累积,导致线程池的内存占用不断增加,最终可能导致应用程序的内存耗尽。
  2. 数据混淆: 内存泄漏会导致线程池中的线程在重用时仍然携带之前任务遗留下的 ThreadLocal 值。这可能会导致任务之间的数据混淆,使得线程在执行任务时使用了不正确的状态,导致错误的结果。
  3. 性能下降: 内存泄漏会导致内存占用持续增加,进而影响垃圾回收的性能。频繁的垃圾回收可能会降低应用程序的性能,甚至可能导致应用程序出现明显的停顿。
  4. 应用程序崩溃: 当内存占用超过系统可用内存时,应用程序可能会因为内存耗尽而崩溃,无法继续执行。

为了避免在线程池中发生内存泄漏,特别是在使用了 ThreadLocal 的情况下:

  • 在任务执行完成后,始终调用 ThreadLocalremove 方法,以确保清除当前线程的 ThreadLocal 副本。

End

总而言之,ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封 装进ThreadLocal,或者把该对象的特定于线程的状态封装进ThreadLocal

当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化我们的程序,使程序更加易读、简洁。ThreadLocal类为各线程提供了存放局部变量的场所。