美文网首页
Threadlocal源码分析

Threadlocal源码分析

作者: 衣忌破 | 来源:发表于2019-05-30 15:41 被阅读0次

    java版本是 java1.8.0_181

    每个版本的具体实现细节都大同小异,接着从以下几个问题进行分析。

    • 为什么要使用ThreadLocal?
    • 怎么使用ThreadLocal?
    • 实现原理?

    一 为什么要使用ThreadLocal?

    JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。一般使用在每个线程独有的一些信息和这些信息又会在多个方法或类中用到。

    比如在Hibernate使用ThreadLocal管理多线程服务的部分。代码如下:

    public static final ThreadLocal session = new ThreadLocal();
        public static Session currentSession() {
       Session s = (Session)session.get();
       //open a new session,if this session has none
       if(s == null){
               s = sessionFactory.openSession();
           session.set(s);
         }
        return s;
     }
    

    session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的s(数据库连接)。当然还有其他的使用场景但不是本文重点先不做介绍。

    二 怎么使用ThreadLocal?

    使用方法其实是挺简单的,在上文的代码已经有写到,下面用更简单的例子介绍。

    public class Test {
        public static void main(String[] s) throws Exception {          
             ThreadLocal<String> tdl = new ThreadLocal<String>();              
             Thread thread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      tdl.set("thread");
                      System.out.println("子线程对应的值  =   "+tdl.get());
                  }
              });
                thread.start();
        }
    }
    

    打印结果:

    子线程对应的值 = thread

    通过ThreadLocal对象就可以通过set和get方法为所在线程分别设置值和获取对应的值。

    三 实现原理?

    看上面代码的set和get方法从直觉上会认为ThreadLocal类应该有一个Object成员变量去接受set方法传递的值以这样的方式去实现给线程设置值,然后get方法就直接返回Object成员变量。事实上真的就这么简单?

    看例子1:

    public class Test {
        public static void main(String[] s) throws Exception {              
             ThreadLocal<String> tdl = new ThreadLocal<String>();              
             
             Thread thread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      tdl.set("thread1");
                      tdl.set("thread2");
                      System.out.println("子线程对应的值  =   "+tdl.get());
                  }
              });
             thread.start();
        }
    }
    

    打印结果

    子线程对应的值 = thread2

    后设置的"thread2"把全面设置的“thread1”覆盖了,似乎看是跟我们猜想的实现方式得出的结果一致。

    再看例子2:

    public class Test {
        public static void main(String[] s) throws Exception {              
             ThreadLocal<String> tdl = new ThreadLocal<String>();              
             tdl.set("main");
             System.out.println("main线程对应的值  =   "+tdl.get());
             
             Thread thread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      tdl.set("thread");
                      System.out.println("子线程对应的值  =   "+tdl.get());
                  }
              });
             thread.start();
             thread.join();// 让子线程先执行完      
             System.out.println("main线程对应的值  =   "+tdl.get());  
        }
    }
    

    如果按上文猜测的ThreadLocal的实现方式的话,最后主线程tdl对象设置的值会被在子线程设置的值覆盖掉,打印的结果应该为:

    main线程对应的值 = main
    子线程对应的值 = thread
    main线程对应的值 = thread

    但实际的结果是:

    main线程对应的值 = main
    子线程对应的值 = thread
    main线程对应的值 = main

    显然在子线程设置的值并没有影响到同一个ThreadLocal对象在主线程设置的值。同一个对象在不同线程上设置的值并没有互相影响,看来ThreadLocal并没有想象的简单。到底是怎么实现这种效果?

    下面从ThreadLocal的set和get开始方法入手去分析ThreadLocal,相信通过这两设置和获取值的方法我们能了解ThreadLocal的实现原理。

    1. ThreadLocal的set方法的实现

    先看set方法的代码:

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

    从代码中我们就可以很容易看出ThreadLocal的set方法的一个大概思路。

    • Thread.currentThread获取当前线程t。
    • 通过getMap方法得出一个应该跟线程t有关系的ThreadLocalMap对象。
    • 判断map不为空,若不为空直接将值设置到map对象中。
    • 判断map为空,创建一个ThreadLocalMap对象并将值设置到该对象中。

    那实现细节到底是怎么样?下面分别通过分析上面set方法中用到的ThreadLocal的getMap(Thread r)、ThreadLocalMap的set(ThreadLocal, Object obj)和ThreadLocal的createMap(Thread r, Object)这三个方法的代码寻找答案。

    1)ThreadLocal的getMap(Thread t)方法

    ThreadLocal的getMap(Thread t)方法代码如下:

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

    可以看出方法是直接返回当前线程的一个ThreadLocalMap对象,显然threadLocals就是一个属于Thread类里面的成员变量。这步很关键通过getMap拿到了Thread对象中可以帮其存储数据的“map”。通过这个“map”ThreadLocal对象就可以将数据设置到Thread线程对象中了。具体是怎么“存”的呢?直接看下文对ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法的分析。

    image.png

    PS: ThreadLocalMap是ThreadLocal的一个静态内部类。

    image.png

    2)ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法
    对应的代码如下:

         private void set(ThreadLocal<?> key, Object value) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);//主义
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    可以看出Threadlocal对象和value会最终存放在Entry对象当中,而对应的Entry对象则会存放ThreadLocalMap对象中的成员变量Entry[ ]数组中。而数组的索引则通过此运算得出key.threadLocalHashCode & (table.length - 1),即将ThreadLocal对象的threadLocalHashCode与Entry的长度减一进行与运算计算出索引值。

    PS:Entry类则是ThreadLocal的内部类:

    image.png

    到这里一共主要提及到ThreadThreadLocalThreadLocalMapEntry这四个类,为避免混淆先梳理他们三者之间的关系是很有必要。如下图

    image.png

    3) ThreadLocal的createMap(Thread t, T firstValue)

    该方法是在map为空的时候调用的,可以看出是在ThreadLocal对象第一次调用set(T value)是会调用该方法。

    createMap(Thread t, T firstValue)的代码如下:

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

    很简单,只是新建一个ThreadLocalMap对象赋值给当前Thread对象的成员变量threadLocals。

    可见Thread类的成员变量ThreadLocal.ThreadLocalMap threadLocals只有在某个ThreadLocal对象在该线程调用createMap(Thread t, T firstValue)时才会被实例化赋值。

    public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
    //线程的threadlocals不为空就不实例化
            if (map != null)
    //设置value设置到map(线程的threadlocals)的成员变量Entry[]数组中
                map.set(this, value);
            else
    //threadlocals为空则实例化并且将value设置到其Entry[]数组中
                createMap(t, value);
        }
    

    接下来看new ThreadLocalMap(this, firstValue)做了什么,代码如下:

       /**
             * Construct a new map initially containing (firstKey, firstValue).
             * ThreadLocalMaps are constructed lazily, so we only create
             * one when we have at least one entry to put in it.
             */
            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    

    注释的意思大概是只有在ThreadLocal对象给线程设置值的时候才会实例化赋值给该线程中的threadLocals。通过代码也可以知道ThreadLocalMap的成员变量Entry[ ] table是在创建ThreadLocalMap的时候才会被创建,并将ThreadLocal对象给线程设置的值存放到table当中。

    小结

    上文涉及的方法间的联系与细节


    a_setvalue.png

    当ThreadLocal对象调用set(T value)方法时,会先获取当前线程对象并通过它获取它的成员变量ThreadLocalMap threadlocals,若threadLocals为空则创建一个ThreadLocalMap对象,threadLocals指向该对象。然后将ThreadLocal对象与value键值对设置进threadLocals中的Entry[ ]数组中。

    将以上总结成一个简单的流程图:


    image.png

    在整个过程中一个Thread线程对应一个ThreadLocal.ThreadLocalMap对象(即其成员变量threadLocals),而threadLocals对应一个数组Entry[ ](即threadLocals中的成员变量table),而table可以存放多个value值并以相对应的ThreadLocal对象间接作为对应value在table中的索引。

    即一个线程对应一个ThreadLocalMap对象,一个ThreadLocalMap对象对应一个Entry[ ]数组,一个Entry数组对应多个<ThreadLocal, T value>键值对。亦即是一个线程对应(保存)多个<ThreadLocal, T value>键值对。

    因此ThreadLocal对象只需要获取到当前线程对象就可以“顺藤摸瓜”将自己和对应的vlaue值传递到不同线程对应的Entry数组中去。

    总的来说线程跟ThreadLocal对象其实就是多对多的关系一个线程可以存多个ThreadLocal对象;而一个ThreadLocal对象可以被存放在多个不同发的ThreadLocal当中去。当然同一个线程是不能存放同一个ThreadLocal多次,当尝试存同样的ThreadLocal对象到Thread中前面的值就会被覆盖掉,正如本文开始所举的例子一。

    如下图对应关系

    image.png

    下面此图就很形象地解析了上文例子2的运行结果原因所在

    image.png

    这就很好解析上文例子2出现的现象。

    2. ThreadLocal的get方法的实现

    其实set方法过程清楚后,get也就很容易理解了。
    先看get方法的代码:

        /**
         * Returns the value in the current thread's copy of this
         * thread-local variable.  If the variable has no value for the
         * current thread, it is first initialized to the value returned
         * by an invocation of the {@link #initialValue} method.
         *
         * @return the current thread's value of this thread-local
         */
        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();
        }
    

    通过阅读注释就可以大概了解get方法的实现过程,其大概意思是

    “该方法会返回当前线程的线程局部变量副本(其实相当于通过set设置的value值)。如果该变量不存在的话就返回调用setInitialValue()的返回值。”

    当map不为空时就调用其getEntry方法并以当前的ThreadLocal对象作为key取得对应的Entry对象,通过Entry对象获取对应的value并返回。

    当map为空时就返回setInitialValue方法的返回值。

    setInitialValue()的代码如下:

       /**
         * Variant of set() to establish initialValue. Used instead
         * of set() in case user has overridden the set() method.
         *
         * @return the initial value
         */
        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;
        }
    

    setInitialValue方法的代码与set方法的代码几乎一模一样唯一的区别是value值来源不同,setInitialValue的value值是通过initialValue()方法获取。

    initialValue方法的代码:

        protected T initialValue() {
            return null;
        }
    

    所以若ThreadLocal对象没有调用set(T value)方法前调用get()方法会返回null不过同时ThreadLocal的threadLocals会指向一个ThreadLocalMap对象。

    相关文章

      网友评论

          本文标题:Threadlocal源码分析

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