• ThreadLocal 验明正身


    一、前言

      之前ThreadLocal使用不多,有个细节也就注意不到了:ThreadLocal在多线程中到底起什么作用?用它保存的变量在每个线程中,是每个线程都保存一份变量的拷贝吗?带着这些问题,我查了几篇博客,又是一个个“罗生门”,很多都贴了ThreadLocal的源码,但是却有些差异,难道是使用的jar不是同一个版本?了解不多,也不敢妄评谁是谁非,于是自己反编译rt.jar进行查看,再结合自己的实例,最终确认ThreadLocal里面存储的并不是某个变量的副本或者拷贝,假如多个线程中用ThreadLocal存储“共享变量”(比如某个对象的引用),那么并不能保证线程安全,其中某个线程改变了这个“共享变量”,那么其他线程使用这个变量的时候也会受影响~

      再引用一下大神对ThreadLocal下的结论:首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。(出处:http://www.iteye.com/topic/103804)(本文引用的源码来自JDK1.6里面的rt.jar包)。

    二、正文

      介绍一下ThreadLocal提供的几个方法(泛型实现是在JDK1.5之后):

      public T get() { }
      public void set(T value) { }
      public void remove() { }
      protected T initialValue() { }

    2.1 get方法实现

    public T get()
      {
        Thread localThread = Thread.currentThread();
        ThreadLocalMap localThreadLocalMap = getMap(localThread);
        if (localThreadLocalMap != null)
        {
          ThreadLocal.ThreadLocalMap.Entry localEntry = localThreadLocalMap.getEntry(this);
          if (localEntry != null)
            return localEntry.value;
        }
        return setInitialValue();
      }

       代码首先获取当前线程,并且通过getMap(T)获取ThreadLocalMap(每个线程都维持有一个ThreadLocalMap类型的变量,名为:"threadLocals";ThreadLocalMap是ThreadLocal的一个静态内部类),如果返回的值不为空,那么返回<key,value>键值对中存储的value,注意,这边的key是“this”,是ThreadLocal实例,而不是当前线程!如果为空,那么调用setInitialValue,并且返回value。

      我们再来看看getMap的实现:

      //getMap
      ThreadLocalMap getMap(Thread paramThread)
       {
          return paramThread.threadLocals;
      }

       大家可以看到,在getMap中,获取的是当前线程的成员变量:threadLocals,在Thread里面,这个成员变量的类型如下:

      ThreadLocal.ThreadLocalMap threadLocals = null;

      这边可以看看ThreadLocalMap的内部实现(红色部分特别注意一下,在1.6的rt.jar中,是super(),不知道是不是我的反编译工具有问题,但是我觉得应该是这边写的这种才对):

    static class ThreadLocalMap{
        static class Entry extends WeakReference<ThreadLocal>{
            Object value;
            Entry(ThreadLocal paramThreadLocal, Object paramObject){
                super(paramThreadLocal);
                value=v;
            }
        }
    }

      这边使用到的WeakReference我会在下一篇博客《Java中的“引用”》中说明,敬请期待~

      接下来我们再来看看get()方法中调用的的setInitialValue实现:

    //setInitialValue
      private T setInitialValue()
       {
          Object localObject = initialValue();
          Thread localThread = Thread.currentThread();
          ThreadLocalMap localThreadLocalMap = getMap(localThread);
          if (localThreadLocalMap != null)
            localThreadLocalMap.set(this, localObject);
          else
            createMap(localThread, localObject);
          return localObject;
       }

      这个代码的第一行可以看到调用了initalValue()方法,这个方法的默认实现是:

    protected T initialValue()
      {
        return null;
      }

      很多时候,我们会去重写这个方法,比如这样:

        private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                return DriverManager.getConnection(DB_URL);
            }
        };
        
        public static Connection getConnection() {
            return connectionHolder.get();
        }

      再来看看,setInitialValue中createMap()的实现:

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

      通过源码的分析,相信大家对ThreadLocal的内部实现已经有所了解,但是看rt.jar里面的源代码,确实没有看到对存储的值进行复制或者拷贝啊,那网上不是很多人说ThreadLocal里面保存的是“变量的拷贝或者变量的副本吗”?小生看来看去,确实无法理解,如果当ThreadLocal里面放的是一个共享的引用类型的变量,那么在多线程环境下,其中一个线程改变了这个共享引用的值,其他线程是否会受影响呢?于是,我就敲了如下代码:

    // Class Jack
    package com.test.main;
    import java.util.HashMap;
    import java.util.Map;
    public class Jack {
        public static ThreadLocal<Map<String,String>> container = new ThreadLocal<Map<String,String>>();
        public static Map<String,String> info = new HashMap<String,String>(){{put("name","Tom");}};
        public void setValue(){
            container.set(info);
        }
    }
    //MyThread
    package com.test.main;
    public class MyThread extends Thread {
        private Jack jack;
        public MyThread(Jack jack,String name){
            super(name);
            this.jack = jack;
        }
        public void run(){
            jack.setValue();
            Thread t = Thread.currentThread();
            String name = t.getName();
            try{
                if("01".equals(name)){
                    Thread.sleep(2000);
                }
                else if("02".equals(name)){
                    Thread.sleep(4000);
                }
                else{
                    Thread.sleep(6000);
                }
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println("线程"+name+":
    原先Jack中name="+jack.container.get().get("name")+"
    "+"现在改为:name="+name+"
    ");
            jack.container.get().put("name", name);
        }
    }
    //Class MainTest package com.test.main; public class MainTest { public static void main(String[] args) { Jack jack = new Jack(); Thread t1 = new MyThread(jack,"01"); Thread t2 = new MyThread(jack,"02"); Thread t3 = new MyThread(jack,"03"); t1.start(); t2.start(); t3.start(); } }

       运行上面MainTest之后的输出如下:

      

      从上面的代码以及运行结果来看,在多线程环境,如果ThreadLocal里面保存的是一个引用类型的变量,还是不能保证线程安全,由此得出如下结论

      首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个(这句话怎么理解,可以看本文提到的重写“initialValue”方法部分),不是什么对象的拷贝或副本

    三、参考

      http://www.cnblogs.com/dolphin0520/p/3920407.html
      http://bbs.csdn.net/topics/380049261
      http://blog.csdn.net/cryssdut/article/details/52123142
      http://blog.csdn.net/lufeng20/article/details/24314381
      http://www.iteye.com/topic/103804
      http://blog.csdn.net/zjclugger/article/details/10940927
      http://www.tuicool.com/articles/imyueq
      http://blog.csdn.net/matrix_xu/article/details/8424038

      

      (备注,本人QQ:1163152850,如有问题欢迎交流,暗号:博客园)

  • 相关阅读:
    ServletConfig类
    坑爹的去哪儿网订酒店经历
    python + opencv + pycharm +语音生成
    最近看到的工作要求
    pip in windows
    路由器外接硬盘做nas可行吗?
    阅读201706
    scrum学习
    学习concurrency programming进展
    Reactor/Proactor的比较 (ZZ)
  • 原文地址:https://www.cnblogs.com/xdouby/p/5897831.html
Copyright © 2020-2023  润新知