Java并发编程之ThreadLocal类

373 阅读2分钟

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

多线程访问同一个共享变量时特别容易出现并发问题,特别是当多个线程对同一个共享变量就行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。ThreadLocal是由JDK包提供的,它提供了线程本地变量,也就是如果是你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存中的变量,从而避免了线程安全问题。

image.png

我们通过下面一个例子来看一下ThreadLocal的使用

public class ThreadLocalTest {


    //print函数
    static void print(String str) {
        System.out.println(str + ":" + localVariable.get());

        localVariable.remove();
    }


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

    public static void main(String[] args) {

        //创建线程one
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程one中本地变量的值
                localVariable.set("threadOne local variable");
                //调用打印函数
                print("threadOne");
                //打印本地变量值
                System.out.println("threadOne remove after" + ":" + localVariable.get());
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("threadTwo local variable");
                print("threadTwo");
                System.out.println("threadOne remove after" + ":" + localVariable.get());
            }
        });

        threadOne.start();
        threadTwo.start();
    }
}

输出结果如下:

image.png

上面代码中,创建了一个ThreadLocal变量localVariale和两个线程threadOne、threadTwo。在这两个线程中通过set方法设置了localVariale的值。从结果中我们可以看出线程中threadOne、threadTwo的变量值是不同的。将代码中的 localVariable.remove() 释取消掉的话结果如下:

image.png

ThreadLocal实现原理: Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals ,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap 。在默认情况下,每个线程中的这两个变量都为 null ,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面而是存放在调用线程的 threadLocals 变量里面。也就是说, ThreadLocal 类型的本地变量不放在具体的线程内存空间中。 ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当线程的 threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。另外, Thread 里面的 threadLocals 为何被设计成 map结构呢?很明显是因为每个线程可关联多个 ThreadLocal 变量。

下面我们来看一下ThreadLocal的set、get、remove方法。

1. void set(T value):

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

上面的代码,首先获取当前的线程,然后使用当前线程作为参数调用getMap(t)方法。

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

可以看到,getMap(t)的作用是获取线程自己的变量threadocals。

然后,如果getMap()返回值不为空,则把value值设置到threadLocals中,也就是把当前变量值放入到当前线程的内存变量threadLocals中。如果getMap()返回值为空,则说明是第一次调用set方法,通过createMap方法创建当前线程的threadLocals变量。

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

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

代码首先线程的threadLocals变量不为空,则设置当前线程的本地变量值为null,则直接返回当前线程绑定的本地变量,否则执行setInitialValue()初始化为默认值。

private T setInitialValue() {
    T value = initialValue(); //null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

如果当前线程的threadLocals变量不为空,则设置当前线程的本地变量值为null,否则调用createMap()方法创建当前线程createMap变量。

3. void remove():

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

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

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

image.png

InheritableThreadLocal类

首先看一下这个例子:

public class ThreadLocalTest1 {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        threadLocal.set("hello world");
        
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread:" + threadLocal.get());
            }
        });
        thread.start();
        System.out.println("main:" + threadLocal.get());
    }
}

image.png

显然同一个ThreadLocal变量在父进程中被设置后,在子线程中是获取不到的。为了解决这个问题,InheritableThreadLocal应运而生。InheritableThreadLocal继承自ThreadLocal,其提供了一个特效,能够让子线程可以访问在父线程中设置的本地变量。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal类通过重写代码让本地变量保存到了具体线程的inheritableThreadLocal变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。