聊一聊ThreadLocal的那些事

1.ThreadLocal的简介

ThreadLocal,是一个提供线程本地变量的类。这些变量不同于普通的变量,每个线程都会初始化一个完全独立的变量副本。ThreadLocal在使用的时候,通常使用 private static来修饰,用以关联一个线程的状态信息。每个线程都能通过get和set方法来获取和设置该线程自己得变量实例,从而实现变量在不同线程之间的隔离,在同一线程共享。

2.ThreadLocal的简单实例

public class Test {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            threadLocal.set("Thread-1");
            System.out.println("t1:" + threadLocal.get());
        });

        Thread t2 = new Thread(() -> {
            threadLocal.set("Thread-2");
            System.out.println("t2:" + threadLocal.get());
        });
        t1.start();
        t2.start();
    }
}

运行结果如下:

t1:Thread-1
t2:Thread-2

3.ThreadLocal的原理

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

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
         table = new Entry[INITIAL_CAPACITY];
          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          table[i] = new Entry(firstKey, firstValue);
          size = 1;
          setThreshold(INITIAL_CAPACITY);
 }

set赋值的时候,先获取当线程,然后获取当前线程的属性ThreadLocalMap,如果ThreadLocalMap存在,就直接以threadLocal为key设置变量值;如果ThreadLocalMap不存在,通过传递当前线程的方法createMap,创建一个ThreadLocalMap作为当前线程Thread的属性。这里面我们最需要注意的就是:
ThreadLocalMap是Thread的一个属性。这一点很重要。
通过上面的源码,我们可以看到,ThreadLocalMap这个类起到了非常重要的作用,下面我们看下ThreadLocalMap是什么?
ThreadLocalMap是一个定制化的Hash Map,仅仅用来维护线程本地值。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
  }

通过上面的源码我们看出,ThreadLocalMap是ThreadLocal的一个内部静态类,使用Entry来保存数据。在Entry保存数据时,使用ThreadLocal来作为key,使用我们设置的value作为value.值得注意的是,Entry使用的key为ThrealLocal 的一个弱引用,这样 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的。到这里我们似乎就霍然开朗了很多,其实所谓的ThreadLocal,它最主要的作用我认为有两个:

  1. 作为ThreadLocalMap的保存数据的key,ThreadLocalMap又是Thread的一个内部的属性(ThreadLocalMap是每个线程都会在调用时(set)进行初始化,即在每个线程中,ThreadLocalMap都会重新实例化后,然后再作为当前线程的属性),这样就实现了是所谓的副本拷贝。
  2. 提供各种调用的api(如set、get和remove)和各种内部的类及方法。
    所以**,ThreadLocal中真正存储数据的是ThreadLocalMap**。

3.2 ThreadLocalMap为什么使用ThreadLocal的弱引用作为key

我们还是看上满Test中的代码,t1、t2两个线程中都引用了ThreadLocal,这是一个强引用,t1又引用ThreadLocalMap,ThreadLocalMap又弱引用了ThreadLocal,如下图所示:
在这里插入图片描述
如果我在t1中加一句:threadLocal == null,那么此时的threadLocal就可以回收了,因为它现在就只有两个弱引用了,而弱引用是在垃圾回收时,是一定会回收的;假如我把弱引用改为强引用,那么此时threadLocal是不可以被回收的,必须要等线程结束之后threadLocal 才会被回收,当线程数过多,线程持续时间较长是,就会有内存溢出的风险。

3.3 ThreadLocal.get()方法的原理

我们先看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();
}

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

    protected T initialValue() {
        return null;
    }

如果存在,就返回ThreadLocalMap中保存的值,如果不存在,就返回初始值null.

3.4 ThreadLocal.remove()方法的原理

顾名思义,就是清除当前线程中的ThreadLocalMap保存的值。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
/**
         * Remove the entry for key.
         */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

4. ThreadLocal的使用场景

  1. ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
    我们知道,SimpleDateFormat是线程不安全的,在多线程使用的时候,需要给每个线程都创建一个SimpleDateFormat实例,如果线程过多,那么就会创建过多的SimpleDateFormat对象。如果我们使用ThreadLocal,那么就只需要创建线程池中所需的数量的SimpleDateFormat实例就可以了。
public class ThreadLocalSimpleDateFormat {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            threadPool.execute(() -> {
                Date date = new Date();
                String data = dateFormatThreadLocal.get().format(date);
                System.out.println(data);
            });
        }
    }
}
  1. ThreadLocal用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
public class ThreadLocalUser {
    public static ThreadLocal<Student> holder = new ThreadLocal<>();

    public static void main(String[] args) {
        Student student = new Student("zhangsan");
        holder.set(student);
        new StudentService().getStudentName();
        holder.remove();
    }
}

class StudentService {
    public void getStudentName() {
        System.out.println(ThreadLocalUser.holder.get().getName());
    }
}

5. ThreadLocal在使用时候的一些坑

5.1 忘记使用remove,造成数据的错乱

在springboot中,我写了这么一个程序:

@RestController
@RequestMapping
public class UserContropller {
    private static final ThreadLocal<Integer> currentUserId = ThreadLocal.withInitial(() -> null);
    @RequestMapping("/user/{userId}")
    public Map getUserById(@PathVariable(value = "userId") Integer userId) {
        Map map = new HashMap<>();

        String before = Thread.currentThread().getName() + ":" + currentUserId.get();
        currentUserId.set(userId);
        String after = Thread.currentThread().getName() + ":" + currentUserId.get();
        map.put("before", before);
        map.put("after", after);
        return map;
    }
}

然后设置tomcat的线程池的线程数为1:server.tomcat.threads.max=1
第一次访问:http://localhost:8080/user/1
结果:

{"before": "[http-nio-8080-exec-1:null](http-nio-8080-exec-1:null)",
"after": "[http-nio-8080-exec-1:1](http-nio-8080-exec-1:1)"
}

显然结果是符合预期的。
第二次访问:http://localhost:8080/user/2
结果:

{
"before": "http-nio-8080-exec-1:1",
"after": "http-nio-8080-exec-1:2"
}

我们看到,结果不符合预期,第一次访问的结果仍然存在。
原因分析:因为我们这个程序是运行在tomcat这种web服务器下,这本身就是运行在一个多线程的环境下,因为线程的创建比较昂贵,所以web服务器往往会使用线程池来处理请求,那么线程就会重用,此时,使用ThreadLocal来存放数据时,就需要显式地区清空设置的数据。
代码修正:

public class UserContropller {

    private static final ThreadLocal<Integer> currentUserId = ThreadLocal.withInitial(() -> null);

    @RequestMapping("/user/{userId}")
    public Map getUserById(@PathVariable(value = "userId") Integer userId) {
        Map map = new HashMap<>();
        String before = Thread.currentThread().getName() + ":" + currentUserId.get();
        currentUserId.set(userId);
        try {
            String after = Thread.currentThread().getName() + ":" + currentUserId.get();
            map.put("before", before);
            map.put("after", after);
            return map;
        } finally {
            currentUserId.remove();
        }
    }
}

5.2 忘记使用remove,造成内存泄露

我们知道,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部的强引用,那么ThreadLocal就会被垃圾回收掉,那么此时ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。假如线程非常多,并且每个线程都执行时间比较长,那么ThreadLocalMap中key为null的Entry中value就会一直存在一条强引用:

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

这就有造成内存泄露的风险。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值