11.ThreadLocal

2018-08-23  本文已影响0人  xialedoucaicai

关于ThreadLocal主要参考了如下几篇文章,对原文作者的部分观点也提出了自己的一些看法,欢迎大家共同讨论。
ThreadLocal趣谈 —— 杨过和他的四个冤家
一个故事讲明白线程的私家领地:ThreadLocal
彻底理解ThreadLocal

1.ThreadLocal是干嘛的

ThreadLocal最核心的作用是将变量与当前线程绑定。

假设我们想在dao层拿到当前登录的用户id,我们会怎么做呢?我们可以在controller从session中获得当前用户id,然后一路传递。

@Controller
public void foo(HttpServletRequest request){
  //从request中获得session,再从session获得用户id
  String id = "";
  //调用service的方法,将id传过去
  service.foo(id);
}

@Service
public void foo(String id){
  dao.foo(id);
}

@Dao
public void foo(String id){
  //终于拿到了id
}

这么做一来是麻烦,万一后面还需要别的参数,那对应的方法全得改。二来可能有些方法你没源码,根本改不了。

当然,你可能会想到使用静态变量,随用随取,这样就不用通过参数一路传递了。

public class Util{
  private static String id = "";
  //get set方法...
}

@Dao
public void foo(){
  //直接取到id  
  System.out.println(Util.getId());
}

这种做法很明显你将面临线程安全问题,你的整个系统不止一个人在用,id一会儿是张三,一会儿又变成李四。可变的静态变量是有大概率出现线程安全问题的,这个一定要小心。

面对这种需求,ThreadLocal就可以大显神威了。我们定义一个静态公用的ThreadLocal对象,每个线程都直接调用set和get方法,都只会取到当前线程对应的变量,相互之间不会影响。

public class ThreadUtil {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
}
@Controller
public void foo(HttpServletRequest request){
  //从request中获得session,再从session获得用户id
  String id = "";
  //将id与当前线程绑定
  ThreadUtil.threadLocal.set(id);
}

@Service
public void foo(){
  dao.foo();
}

@Dao
public void foo(){
  //获取当前线程绑定的id
  String id = ThreadUtil.threadLocal.get();
}

2.ThreadLocal的实现原理

要想正确明白地使用ThreadLocal,一定要看它的源码,弄清楚它到底是如何实现的。

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

可以看到,set方法会先获取到当前线程,然后获取当前线程对象中,一个名为threadLocals的ThreadLocalMap类型的map,然后把自己,也就是threadLocal作为key,把要存储的值作为value,塞入这个map。可以看看Bridge4You公号总结的图,简单明了,为了更好理解,对原图有一点小改动。


ThreadLocal

图中,黄色背景表示属性,白色背景表示属性对应的具体对象。从图中可以看出,Thread中有一个名为threadLocals的属性,该属性也是一个map结构,类型为ThreadLocalMap,key为我们定义的ThreadLocal对象,value为我们调用set方法放入的值。
以上面放用户id为例,如果我们还想放用户名进去,如果继续调用set("张三"),将覆盖之前的用户id,因为key是相同的嘛。这种情况下,我们就要再创建一个ThreadLocal对象了,拿来放name,结合上面的结构图,仔细分析内部的存储方式,应该不难理解。

3.ThreadLocal线程安全?

很多文章都会说到ThreadLocal的另一个作用是可以保证线程安全,因为直接将变量与当前线程绑定了,不就变成单线程了吗,所以线程安全,乍一看貌似有道理。其实ThreadLocal的作用只是将变量与当前线程绑定,至于是不是线程安全,完全取决于你放入其中的对象是否是共享的,我们以很多文章在讲到ThreadLocal的实际应用时,都会提到典型的线程不安全类SimpleDateFormat为例。
这样SimpleDateFormat是线程安全的

public class Foo
{
    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

这样不是线程安全的

public class Foo
{
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return sdf;
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

验证是否线程安全的main方法

public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(4);
    for(;;){
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    new Foo().formatIt(new Date(new Random().nextLong()));
                } catch (Exception e) {
                    e.printStackTrace();
                    threadPool.shutdown();
                }
            }
        }); 
    }
}

上面那种写法是线程安全的。下面那种是不安全的,因为给每个线程绑定的还是同一个SimpleDateFormat,仍然是多线程操作同一个未进行同步处理的共享对象。有很多文章都说ThreadLocal中的value存放的是变量的副本,相互之间不会影响,但根据这个例子来看,并不是这样。

4.要清理ThreadLocal吗?

网上有很多文章都说使用ThreadLocal的时候,要注意在set之后,调用remove清理放入ThreadLocalMap的对象,否则会导致内存泄漏。我个人认为,是否需要调用remove,应该结合当时的使用场景来看。我认为关于内存泄漏,需要考虑以下几点:
1.ThreadLocal对象的数量,因为最终会作为Map的key,如果数量够大,就会导致ThreadLocalMap有很多的key-value,可能导致内存泄漏
2.线程池中的线程的数量,因为线程池中的线程是与整个应用同寿命的,如果创建海量的线程,虽然每个线程里ThreadLocalMap中放的东西不多,但你线程够多的话是有可能会有内存泄漏问题的
3.Map中的value使用频率,如果需要长期使用,且不会占用很多内存,可以考虑初始化时候set一次,而不调用remove方法
4.如果不在线程池的环境下,当然这种情况在正式项目中很少,ThreadLocal中的对象也会随着Thread的消亡而消亡,相当于会自动remove,出现内存泄漏的情况会更少

以上面的需要在dao层获得当前用户的id为例,我定义了一个静态的ThreadLocal对象,这样保证了key固定,每次进入controller都会重新设置新的value,但Map中一直都只有一个key-value。所以在该场景下我即使不调用remove清理,也不会出现任何内存泄漏的问题。

再来看看SimpleDateFormat的例子,静态的ThreadLocal,value为每次都new出来的SimpleDateFormat(),且这一个简单的对象能占10k内存了不得了,一般一个系统的时间格式都是统一的,所以反复remove set反而没必要。在使用线程池的情况下,每个线程都会一直拥有一个独立的SimpleDateFormat对象,既保证了线程安全,代码也显得简洁。

ThreadLocal的Key使用了弱引用,最大限度防止内存泄漏问题,关于这点这里就不详讲了,具体可以参考顶部引用的文章。

综上所述,关于ThreadLocal,它只负责将变量与当前线程进行绑定,至于是否线程安全,完全看你往里面放啥。如果想正确使用ThreadLocal,那就去理解它的实现,结合自己的场景来分析吧。

上一篇下一篇

猜你喜欢

热点阅读