ThreadLocal

作者: hllcve_ | 来源:发表于2018-08-09 23:31 被阅读24次

ThreadLocal就是以内存空间为代价换取时间,它可以为每一个线程提供一份变量,因此可以同时访问并互不干扰

简单理解就是即使共享ThreadLocal,线程不同,ThreadLocal里的值也肯定不同,即使在同一个方法体内,因为ThreadLocal会为每一个线程单独创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,以下面代码为例

public class Test {

    static ThreadLocal<Long> longLocal = new ThreadLocal<>();
    static ThreadLocal<String> stringLocal = new ThreadLocal<>();

    public static void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {

        set();
        System.out.println(longLocal.get());
        System.out.println(stringLocal.get());

        Thread thread = new Thread(() -> {
            set();
            System.out.println(longLocal.get());
            System.out.println(stringLocal.get());
        });

        thread.start();
        thread.join();

        System.out.println(longLocal.get());
        System.out.println(stringLocal.get());
    }
}
输出 1.png

接下来看一下ThreadLocal的源码

set方法
  1. 首先他会获取当前线程
  2. 以当前线程为参数调用getMap方法
  3. 如果获取到map的话就设key和value,key为调用set方法的对象,即ThreadLocal实例,值就是我们要存的T类型的值
  4. 如果没有获取到就会调用createMap方法来创建map
    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
createMap方法,是在给当前线程的静态内部类进行初始化
所以在一个线程中设置longLocal.set(Thread.currentThread().getId())时会对t.threadLocals = new ThreadLocalMap(this, firstValue)进行初始化,而设置stringLocal.set(Thread.currentThread().getName())时则不会再进行初始化,因为他们俩在一个线程里,共用t.threadLocals
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
看一下ThreadLocalMap类,从下图中可以看出ThreadLocalMap是ThreadLocal里的一个静态内部类,所以 t.threadLocals 的类型为 ThreadLocal.ThreadLocalMap
2.png
get方法
  1. 获取当前线程
  2. 获取map
  3. 如果map不为空则获取ThreadLocalMap.Entry,ThreadLocalMap.Entry格式下面图3中
  4. 如果ThreadLocalMap.Entry为空返回null
  5. 如果map为空返回null
/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }
getMap方法
/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
setInitialValue方法
  1. 如果map不为空就把null赋进去
  2. map为空则进行初始化,再把null赋进去
  3. 最后返回null
/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    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;
    }
initialValue方法 就是返回个空值
/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }
3.ThreadLocalMap.Entry
@Slf4j
public class DatabaseContextHolder {

    private static DatabaseType databaseType = null;
    
    public static void setDatabaseType(DatabaseType type) {
        log.info("## setDatabaseType");
        databaseType = type;
    }

    public static DatabaseType getDatabaseType() {
        log.info("## getDatabaseType");
        return databaseType;
    }

}

以上个项目中的DatabaseContextHolder类为例,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,databaseType很可能在setDatabaseType(DatabaseType type)方法中被赋值多次;第二,由于databaseType是共享变量,那么必然在调用databaseType的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用databaseType时,而另外一个线程调用setDatabaseType(DatabaseType type)方法,这样数据源类型被改变,程序难免会出现问题.

因为DynamicDataSource类继承了AbstractRoutingDataSource类,所以在切面方法中将数据源set到用于存放数据源线程安全的容器中DatabaseContextHolder.setDatabaseType(type)后将立马执行重写父类的方法,所以不加同步锁的话就有可能会出现在切面中设置了负责读取的数据源,儿在下面方法获取数据源的时候,有可能被别的线程改成了负责写入的数据源,那样程序就会出现问题

这个方法跟 在切面方法里设置数据源类型是同一个线程,至于为什么,那就要看AbstractRoutingDataSource的源码了

@Override
protected Object determineCurrentLookupKey() {
    logger.info("## determineCurrentLookupKey");
    DatabaseType type = DatabaseContextHolder.getDatabaseType();
    return type;
 }

所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用databaseType的地方需要进行同步处理。
synchronized修饰非静态方法时,锁的是调用该方法的对象
synchronized修饰静态方法时,此时如果调用该静态方法,将会锁住整个类
但是这样将会大大影响程序执行效率,因为一个线程想对数据库进行操作的时候,就要先设置数据源类型,但是由于加锁了的关系,只能等上一个线程结束。

那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

相关文章

网友评论

    本文标题:ThreadLocal

    本文链接:https://www.haomeiwen.com/subject/lhzrpftx.html