如何解决ThreadLocal因线程复用导致失效的问题?

程序员考拉

共 3927字,需浏览 8分钟

 · 2020-08-18


ThreadLocal 提供一个线程本地变量, 顾名思义, 这个变量值与线程相关, 不同线程之间互不影响.


它是如何做到这点的呢? 我们来看下源码.


public class ThreadLocal {
    protected T initialValue() {
        return null;
    }

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

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

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

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

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

public class Thread implements Runnable {

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */

    ThreadLocal.ThreadLocalMap threadLocals = null;
}


上面只截取了一部分源码, 我们主要看get(), set()和remove()方法. 当我们调用get()方法时, 首先获取当前线程, 然后获取该线程的 ThreadLocalMap 对象, 如果 ThreadLocalMap 为null, 则调用初始化方法, 否则从 ThreadLocalMap 中获取值. 当我们调用set()方法时, 和get()方法步骤类似, 会把值设置到当前线程的 ThreadLocalMap 对象中.


通过看源码我们知道, get()和set()方法的第一步都是获取当前线程的 ThreadLocalMap 对象, 且值是存储在这个对象中的, 所以我们说 ThreadLocal 提供一个线程本地变量, 这个变量值与线程相关, 不同线程之间互不影响.


remove()方法用来清除这个线程本地变量的值, 这个方法很容易被忽视, 其实我们在进行get()和set()操作之后, 需要调用remove方法来清除这个线程本地变量的值, 否则会因线程复用导致ThreadLocal失效. 我们来看下面两个的代码:


(1) 代码一


public class Test{

    public static void main(String[] args) {
        final ThreadLocal dataSourceTypes = new ThreadLocal(){
            @Override
            protected String initialValue()
{
                return "master";
            }
        };

        Executor executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run()
{
                    System.out.println(dataSourceTypes.get());
                    dataSourceTypes.set("slave");
                }
            });
        }
    }
}
结果:
master
master
master
master
master


(2) 代码二


public class Test{

    public static void main(String[] args) {
        final ThreadLocal dataSourceTypes = new ThreadLocal(){
            @Override
            protected String initialValue()
{
                return "master";
            }
        };

        Executor executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 8; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run()
{
                    System.out.println(dataSourceTypes.get());
                    dataSourceTypes.set("slave");
                }
            });
        }
    }
}

结果:
master
master
master
master
slave
master
slave
slave

我们对比下上面两个代码, 代码一的开启的线程数与线程池中的线程数相等, 得到了我们想要的结果. 而代码二开启的线程数大于线程池中的线程数, 导致结果与我们期望的不一样, 为什么会出现这种情况呢?


看了ThreadLocal的源码之后, 我们知道ThreadLocal是与线程相关的, 代码二中线程池里只有5个线程, 而我们开启了8个线程, 这就会导致线程复用, 在第一次调用get()方法时发现当前线程的ThreadLocalMap里有值, 从而导致不会调用setInitialValue()方法.


这种线程复用的问题在实际开发中会出现吗? 当然, 比如我们的Tomcat服务器里存在一个线程池, 对于每一个http请求, 都会从线程池中取出一个空闲线程, 但这个线程池的默认线程只有75个, 超过后一样会出现线程复用.


所以为了解决这种线程复用的问题, 我们在开发中使用ThreadLocal的最后要调用remove()方法.


原文链接:csdn.net/litianxiang_kaola/article/details/90169600




浏览 65
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报