首页 > 技术文章 > 正确理解ThreadLocal:ThreadLocal中的值并不一定是完全隔离的

moxiaotao 2018-09-12 09:38 原文

首先再讨论题主的这个观点之前我们要明确一下ThreadLocal的用途是什么?

ThreadLocal并不是用来解决共享对象的多线程访问问题。

看了许多有关ThreadLocal的博客,看完之后会给人一种错觉,ThreadLocal就是用于在多线程情况下防止共享对象的线程安全问题,使用ThreadLocal之后,ThreadLocal的对象就不会有线程安全问题,但是一定是这样么,看如下代码

  1.  
    public class test {
  2.  
    public static void main(String[] args) throws InterruptedException {
  3.  
    new A().start();
  4.  
    new A().start();
  5.  
    new A().start();
  6.  
    new A().start();
  7.  
    }
  8.  
     
  9.  
    static class A extends Thread {
  10.  
    static List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
  11.  
    static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<List<Integer>>() {
  12.  
    @Override
  13.  
    protected List<Integer> initialValue() {
  14.  
    return list;
  15.  
    }
  16.  
    };
  17.  
     
  18.  
    @Override
  19.  
    public void run() {
  20.  
    List<Integer> threadList = threadLocal.get();
  21.  
    threadList.add(threadList.size());
  22.  
    System.out.println(threadList.toString());
  23.  
    }
  24.  
     
  25.  
    }
  26.  
    }

该代码很简单,就是在多线程的情况下输出ThreadLocal的list集合状态,如果此时线程安全的话,输出的4个语句应该是完全一样的输出结果如下:

[1, 2, 3, 4, 5, 5]
[1, 2, 3, 4, 5, 5, 6]
[1, 2, 3, 4, 5, 5, 6, 7]

[1, 2, 3, 4, 5, 5, 6, 7, 8]

 

很明显可以看到,线程是不安全的,从结果也能看出来4个线程中ThreadLocal中的List是同一个对象,被四个线程所共享。

接下来我们分析一下原因的发生原因,我们去看ThreadLocal的get()方法

  1.  
    public T get() {
  2.  
    Thread t = Thread.currentThread();
  3.  
    ThreadLocalMap map = getMap(t);
  4.  
    if (map != null) {
  5.  
    ThreadLocalMap.Entry e = map.getEntry(this);
  6.  
    if (e != null) {
  7.  
    @SuppressWarnings("unchecked")
  8.  
    T result = (T)e.value;
  9.  
    return result;
  10.  
    }
  11.  
    }
  12.  
    return setInitialValue();
  13.  
    }

如果一个线程第一次调用threadLocal.get()方法时,我们通过调试可以发现此时拿到的map是null,会调用setInitialValue(),继续看该方法

  1.  
    private T setInitialValue() {
  2.  
    T value = initialValue();
  3.  
    Thread t = Thread.currentThread();
  4.  
    ThreadLocalMap map = getMap(t);
  5.  
    if (map != null)
  6.  
    map.set(this, value);
  7.  
    else
  8.  
    createMap(t, value);
  9.  
    return value;
  10.  
    }

可以看到这个方法里面有一个value,而这个value就是ThreadLocal中即将要保存的只对线程所见的"副本",而这个value使用过initialValue()方法得到的,这个方法如果你没有重写的话默认返回时一个null,而在我们的例子当中他返回的是我们定义的静态变量list,而这个list是一个单例的(只会被类第一次访问时进行初始化),也就是说我们的例子中每个线程的ThreadLocal里面get的都是同一个list,所以就造成了线程安全的问题.

接下来改善一下我们的代码

  1.  
    static class A extends Thread {
  2.  
    static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<List<Integer>>() {
  3.  
    @Override
  4.  
    protected List<Integer> initialValue() {
  5.  
    return new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
  6.  
    }
  7.  
    };
  8.  
     
  9.  
    @Override
  10.  
    public void run() {
  11.  
    List<Integer> threadList = threadLocal.get();
  12.  
    threadList.add(threadList.size());
  13.  
    System.out.println(threadList.toString());
  14.  
    }
  15.  
     
  16.  
    }

运行结果

[1, 2, 3, 4, 5, 5]
[1, 2, 3, 4, 5, 5]

[1, 2, 3, 4, 5, 5]

[1, 2, 3, 4, 5, 5]

 

接下来回到主题,ThreadLocal的作用是什么:

ThreadLocal使得各线程能够保持各自独立的一个对象,而实现原理其实是通过,每个线程都会重新创建一个对象,不是什么对象的拷贝或副本,而线程是否安全取决于你如何去创建这个对象。

然后总结一下:

1.ThreadLocal作用不是为了解决共享对象的多线程安全问题,而是为了避免通多参数传递的方式去拿到一个对象,网上有些例子就一开始拿线程安全举例子,然后抛砖引玉出ThreadLocal,会把人带偏。。。博主就是一个。

2.ThreadLocal中保存的不是什么对象的副本,里面没有需要保存的对象做任何复制,拷贝操作,这个对象完全取决于你的iniialtValue方法中如何去创建,所以这里需要考虑使用ThreadLocal的性能问题,是否会大量创建一个对象。

推荐阅读