Java多线程-ThreadLocal详解

2,477 阅读6分钟

简介

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap,把数据进行隔离,每个线程的数据不共享,自然就没有线程安全方面的问题了.

ThreadLocal可以实现每个线程绑定自己的值,即每个线程有各自独立的副本而互相不受影响。一共有四个方法:get, set, remove, initialValue。可以重写initialValue()方法来为ThreadLocal赋初值

例子

本例开启了两个线程,在每个线程内部都设置了本地变量的值,代码如下:

public class ThreadLocalDemo {

    //创建ThreadLocal变量
    static ThreadLocal<String> localParam = new ThreadLocal<>();

    public static void main(String[] args) {
        //创建2个线程,分别设置不同的值
        new Thread(()->{
            localParam.set("hello java");
            //打印当前线程本地内存中的localParam变量的值
            System.out.println(Thread.currentThread().getName() + ":" + localParam.get());
        },"T1").start();
        new Thread(()->{
            localParam.set("hello web");
            System.out.println(Thread.currentThread().getName() + ":" + localParam.get());
        },"T2").start();
    }
}

结果:

T1:hello java
T2:hello web

说明每个线程都会有这个变量的本地副本,当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量。

实现原理

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。

Thread类中有两个ThreadLocalMap类型的变量,分别是threadLocals和inheritableThreadLocals,而ThreadLocalMap是一个定制化的Hashmap,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。

ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用

如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。

另外Thread里面的threadLocals被设计为map结构是因为每个线程可以关联多个ThreadLocal变量。

常用方法详解

set方法

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //将当前线程作为key,去查找对应的线程变量,找到则设置
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    //第一次调用就创建当前线程对应的ThreadLocalMap
        createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果getMap(t)的返回值不为空,则把value值设置到threadLocals中,也就是把当前变量值放入当前线程的内存变量threadLocals中。threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值 。

如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。

get方法

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //如果threadLocals不为null,则返回对应本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //threadLocals为空则初始化当前线程的threadLocals成员变量
    return setInitialValue();
}

private T setInitialValue() {
    //初始化为Null
    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;
}

如果ThreadLocal没有调用set方法,直接调用get方法,则会返回null.

remove方法

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
}

如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量。

ThreadLocal的作用

ThreadLocal归纳下来就2类用途:

  • 保存线程上下文信息,在任意需要的地方可以获取.

由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。

还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.

局限性:每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的,但是ThreadLocal无法解决共享对象的更新问题!

把ThreadLocal定义为static还有一个好处就是,由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除!

最佳实践做法应该为:

try {
    // 其它业务逻辑
} finally {
    threadLocal对象.remove();
}

注意事项

注意:在线程池的情况下,在ThreadLocal业务周期处理完成时,最好显式的调用remove()方法,清空”线程局部变量”中的值。正常情况下使用ThreadLocal不会造成内存溢出,弱引用的只是threadLocal,保存的值依然是强引用的,如果threadLocal依然被其他对象强引用,”线程局部变量”是无法回收的。

总结

在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。