ThreadLocal原理

ThreadLocal含义

ThreadLocal线程本地变量把变量与线程绑定在一起,为每一个线程维护一个独立的变量副本(因为是对象引用,堆中的对象是线程间共享的,所以ThreadLocal没有解决线程安全问题),在本线程内随时可取。而ThreadLocal实例通常是private static类型的,用于关联线程。

原理

public class Thread implements Runnable {
    省略
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal 包含了静态内部类ThreadLocalMap,Thread使用了ThreadLocalMap。
ThreadLocalMap包含静态内部类Entry,还包含Entry数组(每个数组元素是ThreadLocal+value一对,threadLocal对象是弱引用,GC时自动回收)。
ThreadLocal的整体结构

引用关系

除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
ThreadLocal对象不一定在堆上。如果ThreadLocal被定义成了static的,那么ThreadLocal对象是类共用的,可能出现在方法区。

为什么用ThreadLocal做key?

ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
如果应用中一个线程只使用一个ThreadLocal对象,那么使用Thread做key也可以。

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}

如果应用中一个线程不只使用了一个ThreadLocal对象,那么使用Thread做key有问题?

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}

通过Thread对象,无法知道要获取哪个ThreadLocal对象?

Entry的key为什么设计成弱引用?

假如key对ThreadLocal对象的弱引用改为强引用。

ThreadLocal变量对ThreadLocal对象是强引用。即使ThreadLocal变量设置成null,但key对ThreadLocal还是强引用。如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁,那么就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。ThreadLocal对象和ThreadLocalMap都不会被GC回收,产生了内存泄露问题。
弱引用的对象,GC时回收。即如果key是弱引用,当ThreadLocal变量指向null之后,GC时回收key,其值设置成null。

ThreadLocal变量指向null,调用它的get、set或remove方法,会出现空指针异常。如果另外一个ThreadLocal变量b调用了它的get、set或remove,触发清理机制,将key为null的value值清空。如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了,在最大程度上解决内存泄露问题。
Entry中key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有指向null,GC把弱引用的key回收了,会影响使用。如果当前ThreadLocal变量指向null,并且key也为null了,没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。

弱引用的例子

public static void main(String[] args) {
    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
    System.out.println(weakReference0.get());
    System.gc();
    System.out.println(weakReference0.get());
}

运行结果

java.lang.Object@28d93b30
null

传入WeakReference构造方法的是直接new出来的对象,没有其他引用,在调用gc方法后,弱引用对象会回收。

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
}

运行结果

java.lang.Object@28d93b30
java.lang.Object@28d93b30

先定义一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。gc后弱引用对象不会回收。
Entry对象中的key属于第2种情况。

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());

    object=null;
    System.gc();
    System.out.println(weakReference1.get());
}

运行结果

java.lang.Object@28d93b30
java.lang.Object@28d93b30
null

如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。

Entry的value为什么不设计成弱引用?
如果Entry的value只是被Entry引用,没被业务系统中的其他地方引用,那么value是弱引用时GC回收后会导致系统异常。相比之下,Entry的key指向的是ThreadLocal。

ThreadLocal如何导致内存泄露?

 

如果ThreadLocalMap中存在很多key为null的Entry,没有调用过有效的ThreadLocal的get、set或remove方法,那么Entry的value值不会被清空,
存在一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。Entry和ThreadLocalMap会长期存在下去,会导致内存泄露。

如何解决内存泄露问题?

在使用完ThreadLocal对象之后调用它的remove方法,remove方法会把Entry中的key和value都设置成null。

public class Main {
    public String get() {
        try{
            return CurrentUser.get();
        } finally {
            CurrentUser.remove();
        }
    }

    public static void main(String[] args) {
        CurrentUser.set("abc");
        System.out.println(new Main().get());
        System.out.println(new Main().get());
    }

    static class CurrentUser {
        private static final ThreadLocal<String> THREA_LOCAL = new ThreadLocal<>();

        public static void set(String str) {
            THREA_LOCAL.set(str);
        }

        public static String get() {
            return THREA_LOCAL.get();
        }

        public static void remove() {
            THREA_LOCAL.remove();
        }
    }
}

运行结果

abc
null

ThreadLocal是如何定位数据的?

ThreadLocal的get、set、remove方法中都有这样一行代码:
int i = key.threadLocalHashCode & (len-1);
假设len=16,key.threadLocalHashCode=31
int i = 31 & 15 = 15相当于int i = 31 % 16 = 15
Entry数组长度是2^n,位运算效率更高。

1.通过key的hashCode取余计算出一个下标。
2.通过下标定位具体Entry,如果找到了,那么返回。
3.如果第2步没有找到,那么从数组的下标位置继续往后找,遇到最后一个位置时从头开始继续找。
4.直到找到第一个Entry为空为止。

ThreadLocal有哪些用途?

1.在Spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
2.获取当前登录用户上下文。
3.临时保存权限数据。

代码举例

场景:有5个线程,这5个线程都有一个值value,初始值为0,线程运行时用一个循环往value值相加数字。

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new MyThread(i)).start();
        }
    }
 
    static class MyThread implements Runnable {
        private int index;
 
        public MyThread(int index) {
            this.index = index;
        }
 
        public void run() {
            System.out.println("线程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("线程" + index + "的累加value:" + value.get());
        }
    }
}

运行结果

线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45

参考资料

ThreadLocal夺命11连问

posted on 2023-01-23 14:04  王景迁  阅读(72)  评论(0编辑  收藏  举报

导航