线程本地变量-ThreadLocal

144 阅读3分钟

一、问题引入

多线程访问同一个共享变量时特别容易出现并发问题,尤其是多个线程需要对一个共享变量进行写入时。

可以通过以下方法保证线程安全

  • 加锁

加锁同步.drawio.png

  • 使用线程本地变量,也就是ThreadLocal

线程本地变量.drawio.png

二、ThreadLocal原理

ThreadLocal.drawio.png

  • Thread类中有一个threadLocals和一个inheritableThreadLocals,都是ThreadLocalMap类型的变量
  • ThreadMap是一个定制化的Hashmap
  • 当前线程第一次调用ThreadLocal的set/get方法才会创建这两个变量
  • ThreadLocal类型的本地变量存放在具体的线程内存空间中,ThreadLocal是一个工具壳,通过set方法把value值放入调用线程的treadLocals;通过get方法从当前线程的threadLocals变量将其拿出来
  • 可以调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量

Question:Thread里面的threadLocals为什么要设计为Map结构?

因为每个线程可以关联多个ThreadLocals变量。

- 源码分析

set()方法

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


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


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

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;
        }
    }
    // treadLocals为null则初始化当前线程的threadLocals成员变量
    return setInitialValue();
    
   
private T setInitialValue() {
    // 初始化为null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果当前线程的threadLocals变量不为空
    if (map != null)
        map.set(this, value);
    // 如果当前线程的threadLocals变量为空
    else
        createMap(t, value);
    return value;
}

remove()方法

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

Question:为什么在不使用本地变量之后需要对其remove掉?

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

PS:对应线程的声明周期,一个线程运行结束就是销毁状态。

三、代码实现

public class TestThreadLocal {

    // print函数
    static void print(String str){
        // 打印当前线程本地内存中变量的值
        System.out.println(str+":"+localVariable.get());
        // 清除当前线程本地内存中的localVariable变量
        // localVariable.remove();
    }

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

    public static void main(String[] args) {
        // 创建线程Thread1
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置thread1中本地变量localVariable的值
                localVariable.set("thread1 local Variable");
                // 调用答应函数
                print("thread1");
                // 打印本地变量值
                System.out.println("thread1 remove after:"+localVariable.get());
                // 睡眠1s后再次打印看变量是否被覆盖
                try {
                    Thread.sleep(1000);
                    System.out.println("thread1 remove after:"+localVariable.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置thread1中本地变量localVariable的值
                localVariable.set("thread2 local Variable");
                // 调用打印函数
                print("thread2");
                // 打印本地变量值
                System.out.println("thread2 remove after:"+localVariable.get());
                // 睡眠1s后再次打印看变量是否被覆盖
                try {
                    Thread.sleep(1000);
                    System.out.println("thread1 remove after:"+localVariable.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }

}

image.png 可以看到,线程1和线程2各自对线程本地内存的变量进行操作,互不影响,是线程安全的。

把删除本地变量的注释取消掉并取消休眠:

image.png

四、一个应用

在SpringBoot中注入HttpSertletRequest获取会话信息,而HttpServletRequest是一个ThreadLocal变量,所以在多线程环境下,各个线程操作的都是自己本地的副本,是线程安全的。