美文网首页
11.ThreadLocal

11.ThreadLocal

作者: xialedoucaicai | 来源:发表于2018-08-23 07:47 被阅读0次

    关于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,那就去理解它的实现,结合自己的场景来分析吧。

    相关文章

      网友评论

          本文标题:11.ThreadLocal

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