ThreadLocal源码解析

本篇博客的目录:

一:ThreadLocal的简介

二:ThreadLocal源码分析

三:ThreadLocal实例

四:总结

一:ThreadLocal的简介

1.1:简单解释

      ThrealLocal望文生义,简单解释就是线程的本地变量。我们来看一下jdk对它的定义:该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 getset 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。从这段解释中可以看出它是实现线程安全的一种新的方式,不同于以前加锁synchronized的方式,每个线程都有自己的独立的变量,这样他们之间是互不影响的,这样就间接的解决线程安全的问题。举个通俗的例子,相当于由以前的贫穷年代,大家哄抢一块蛋糕,到现在的物质丰盛年代,每个人都有一块自己的蛋糕,大家互不影响,就不会存在争抢的情况。

1.2:ThreadLocal的方法摘要

从jdk看ThreadLocal向外暴露出基本的增删改查方法,几个方法都是很简单,通过get和set方法是访问和修改的入口,再通过initialValue进行初始化值和remove方法移除值。


get()
返回此线程局部变量的当前线程副本中的值。
  initialValue()
返回此线程局部变量的当前线程的“初始值”。
  remove()
移除此线程局部变量当前线程的值。
  set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。

二:ThreadLocal的源码分析

2.1:误解读之处

很多人以为ThreadLocal的内部维持了一个map,其中以当前线程作为键,传入的数据作为值来进行封装的,这个想法是错误的。ThreadLocal的内部维护着一个叫做ThreadLocalmap的静态类,它由一个首位闭合的动态数组组成(默认大小为16),每个数组都是一个Entry对象,该对象以ThreadLocal对象作为key,以传入的数据作为值进行封装而成。以下是TheadLocalMap的图示:

 

 

2.2:关于Entry对象

        static class Entry extends WeakReference<ThreadLocal> {//用虚引用封装的ThreadLocal
            Object value;//声明一个值
            Entry(ThreadLocal k, Object v) {//用ThreadLocal对象和值创建一个Entry
                super(k);
                value = v;
            }
        }

该对象继承自弱引用WeakReference,我们知道java中引用类分为4种,强度从大到小的排列顺序为:强引用、软引用、弱引用、虚引用。这样做的最大好处就是可以方便GC处理

其中关于弱引用: 具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

所以可以调用get方法获取对应的泛型值ThreadLocal,下面很多都会用到get方法,它来自于Reference类,返回T:为泛型引用类型。ThreadLocal本身有一个构造方法,为TheadLocal和value。

2.2:几个重要方法解读

2.2.1:get方法源码

public T get() {  //get方法
        Thread t = Thread.currentThread();//获取当前的线程
        ThreadLocalMap map = getMap(t);//根据线程获取线程本地的map
        if (map != null) {//如果map不为null
            ThreadLocalMap.Entry e = map.getEntry(this);//根据threadlocal对象获取Entry
            if (e != null)//如果键值映射对象不为null
                return (T)e.value;//返回获取它的值
        }
        return setInitialValue();//如果map里面为null 返回setInitialValue方法
    }

首先是先获取当前运行的线程,再通过线程来获取ThreadLocalMap对象,我们来看一下getMap(Thread)这个方法:

ThreadLocalMap getMap(Thread t) {//根据线程获取本地的map
        return t.threadLocals;//返回线程的线程本地变量
    }
  ThreadLocal.ThreadLocalMap threadLocals = null;

这里就是通过Thread来间接引用ThreadLocal,再引用ThreadLocalMap,从而达到通过线程来获取ThreadLocalmap的目的。然后判断该map是否为null,不为null的情况下获取Entry对象,以下是getEntry对象的源码:

  private Entry getEntry(ThreadLocal key) { //根据ThreadLocal作为key键获取Entry
            int i = key.threadLocalHashCode & (table.length - 1);//根据键的HashCode值与运算数组的长度-1获取一个位置
            Entry e = table[i];//得到该位置上的节点对象
            if (e != null && e.get() == key)//如果该对象不为null并且通过get方法获取对应的值判断其是否等于传入的key
                return e;//如果两个条件成立 返回该节点
            else
                return getEntryAfterMiss(key, i, e);//否则调取getEntryAfterMiss方法
        }

可以看出它考虑到了哈希碰撞的情况,这个在HashMap源码分析篇也讲解过了,因为在遍历set值的时候考虑到哈希碰撞的问题(一个节点对应两个值),一般会取key的hashcode值和数组的长度-1(默认情况下是15)进行与运算,获取一个数组的位置,将其放入到该节点的位置。这里相当是一个逆运算,省去了遍历的性能开销问题,直接取该节点上的值。当然还有getEntryAfterMiss方法是为了解决出现了hash碰撞的问题,以下是getEntryAfterMiss方法:

  private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { //通过hash直接找不到对应的值调用此方法
            Entry[] tab = table;//获取数组
            int len = tab.length;//得到数组的长度
            while (e != null) {//循环直到节点Entry对象不为null
                ThreadLocal k = e.get();//获取键值ThreadLocal
                if (k == key)//如果key值相同
                    return e;//返回该节点
                if (k == null)//如果key为null
                    expungeStaleEntry(i);//调取exoungeStaleEntry方法
                else
                    i = nextIndex(i, len);//节点顺移
                e = tab[i];//取下一节点值赋给该节点
            }
            return null;//否则返回null
        }

该方法主要是相当于一个遍历(While循环)比较key来获取值的过程,从中可以看出ThreadLocalMap是允许键为null的,当键为null的情况下,调用expungeStaleEntry方法进行GC处理,便于垃圾回收器回收键为null的数组元素。

2.2.2:set方法源码

 private void set(ThreadLocal key, Object value) {
            Entry[] tab = table; //获取数组
            int len = tab.length;//得到数组的长度
            int i = key.threadLocalHashCode & (len-1);//获取键值的hashCode与数组的长度-1进行与运算

            for (Entry e = tab[i];//遍历循环整个数组
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();//得到键值

                if (k == key) {//如果键和传入的键相同(也就是传入的key已经存在了)
                    e.value = value;//用新值覆盖旧值
                    return;//结束该方法
                }

                if (k == null) {//如果键为null
                    replaceStaleEntry(key, value, i);//调用replaceStaleEntry方法
                    return;//结束该方法
                }
            }

            tab[i] = new Entry(key, value);//把传入的键和值进行构造新建Entry对象
            int sz = ++size;//size+1赋值为sz
            if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果数组中没有冗余的null值并且如果size大于临界值
                rehash();//扩容重新hash
        }

 当进行set值的时候,首先是计算键位(通过key的HashCode值和数组的长度-1进行与运算),然后检查数组中有没有和传入的key相同的键值,如果有的话就用新值覆盖掉旧值,然后结束该方法。如果key为null时,就调用replaceStaleEntry方法清除掉null值,两个情况都没的话,新构建一个Entry对象,放入到计算出的键位中,并且把数组的长度+1.再判断没有null值的情况下,并且数组的大小超过临界值了就进行重hash的操作:

我们来看一下rehash的源码:可以看出是先进行处理Entry值,然后再进行重建size:

 private void rehash() {
            expungeStaleEntries();//移除不用的entry

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4) //如果size大于等于临界值-临界值的4分之一(这里相当于是12)
                resize();//扩容
        } 
private void expungeStaleEntries() { //移除不用的entry从而达到自动释放内存的目的
            Entry[] tab = table;//复制整个数组
            int len = tab.length; //得到数组的长度
            for (int j = 0; j < len; j++) {//遍历循环整个数组
                Entry e = tab[j];//获取数组中的元素
                if (e != null && e.get() == null)//如果元素不为null并且获取的键为null
                    expungeStaleEntry(j);//调用expungeStaleEntry方法

 } }
     private int expungeStaleEntry(int staleSlot) {//自动释放内存
            Entry[] tab = table; //获取数组
            int len = tab.length;//得到数组的长度

            // expunge entry at staleSlot
            tab[staleSlot].value = null;//数组的对应节点值设为nulll
            tab[staleSlot] = null;//数组对应的节点设为null
            size--;//数组大小-1

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);//for循环遍历
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) { //移动到下一节点
                ThreadLocal k = e.get();//获取键
                if (k == null) {//如果键不为null
                    e.value = null;//value值不为null
                    tab[i] = null;//数组节点的值设为null
                    size--;//size-1
                } else {
                    int h = k.threadLocalHashCode & (len - 1);//键的hashcode值与数组长度-1进行与运算获取一个位置
                    if (h != i) {//如果该位置不是下一节点
                        tab[i] = null;//数组的元素设为null
                        while (tab[h] != null)//
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

  private void resize() {
            Entry[] oldTab = table;//得到旧数组
            int oldLen = oldTab.length;//得到旧数组长度
            int newLen = oldLen * 2;//旧数组长度乘以2
            Entry[] newTab = new Entry[newLen];//新建一个数组 长度为旧数组的2倍
            int count = 0;//定义count为0

            for (int j = 0; j < oldLen; ++j) {//循环遍历旧数组
                Entry e = oldTab[j];//获取久数组的节点
                if (e != null) {//如果不为null
                    ThreadLocal k = e.get();//获取键值
                    if (k == null) {//如果键为bull
                        e.value = null; // 值也设为null
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);//通过hashcode值计算键位
                        while (newTab[h] != null)//键位移动直到不为null
                            h = nextIndex(h, newLen);
                        newTab[h] = e;//给数据元素设值
                        count++;//count进行+1
                    }
                }
            }

            setThreshold(newLen);//设置临界值为新数组的长度
            size = count;//大小为count值
            table = newTab;//将新数组替换过去的旧数组
        }

扩容的过程是原来数组长度的2倍,也就是说现在是16,接下来就是32,再然后就是64...,再新建一个新Entry数组,把不为null的的元素放进去新数组,放入的位置为根据键的HashCode和长度-1进行与运算后的值,再接着遍历循环设置值。后面再把临界值扩大,size大小重设,维护的新数组重设就完成了

2.2.3:remove方法源码

 private void remove(ThreadLocal key) { //根据键移除对应的值
            Entry[] tab = table;//复制整个数组
            int len = tab.length;//得到数组的长度
            int i = key.threadLocalHashCode & (len-1);//根据HashCode值计算键位
            for (Entry e = tab[i];//遍历循环整个数组
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {//如果找到的值和键相同
                    e.clear();//调用reference类中的clear方法将引用置为null
                    expungeStaleEntry(i);//除去不用的null值
                    return;//结束该方法
                }
            }
        }

remove方法是根据传入的ThreadLocal作为键,然后去计算键位,再从键位的下一个index值开始进行逐个遍历,直到找到的值和键相同,就调用reference的clear方法将引用置为null,并且清除无用的null值,然后结束该方法。

三:ThreadLocal使用实例

3.1:用ThreadLocal解决SimpleDateFormat的线程不安全的问题

   simpleDateFormate是一个线程不安全的格式化日期类,创建一个 SimpleDateFormat实例的开销比较昂贵,解析字符串时间时频繁创建生命周期短暂的实例导致性能低下。即使将 SimpleDateFormat定义为静态类变量,貌似能解决这个问题,但是SimpleDateFormat是非线程安全的,同样存在问题,如果用 ‘synchronized’线程同步同样面临问题,同步导致性能下降(线程之间序列化的获取SimpleDateFormat实例)。可以使用Threadlocal解决了此问题,对于每个线程SimpleDateFormat不存在影响他们之间协作的状态,为每个线程创建一个SimpleDateFormat变量的拷贝:

public class DateUtil {  
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";  
    private static ThreadLocal threadLocal = new ThreadLocal(){  
          protected synchronized Object initialValue() {  
                return new SimpleDateFormat(DATE_FORMAT);  
            }  
    };  
  
    public static DateFormat getDateFormat() {  
        return (DateFormat) threadLocal.get();  
    }  
  
    public static Date parse(String textDate) throws ParseException {  
        return getDateFormat().parse(textDate);  
    }  

 3.2:使用ThreadLocal实现数字自增

public class AutoAddNumber {

    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {

        public Integer initialValue() {

            return 0;
        }

    };

    public int getNextNum() {

        seqNum.set(seqNum.get() + 1);

        return seqNum.get();
    }
}

线程类:TestThreadLocalThread维护一个AutoAddNumber引用:

public class TestThreadLocalThread extends Thread {

    private AutoAddNumber an;

    public TestThreadLocalThread(AutoAddNumber an) {
        this.an = an;
    }

    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println("当前线程是:" + Thread.currentThread().getName() + "对应的编号是:" + an.getNextNum());
        }
        
    }

}

测试类,启动4个线程,每个线程在自己的run方法里循环遍历10次然后进行输出:

public class Test {
    
    public static void main(String[] args) {

        AutoAddNumber an = new AutoAddNumber();

        TestThreadLocalThread testThread1 = new TestThreadLocalThread(an);

        TestThreadLocalThread testThread2 = new TestThreadLocalThread(an);

        TestThreadLocalThread testThread3 = new TestThreadLocalThread(an);
        
        TestThreadLocalThread testThread4 =new TestThreadLocalThread(an);

        testThread1.start();

        testThread2.start();

        testThread3.start();
        
        testThread4.start();

    }

}

输出结果:

当前线程是:Thread-0对应的编号是:1
当前线程是:Thread-0对应的编号是:2
当前线程是:Thread-0对应的编号是:3
当前线程是:Thread-0对应的编号是:4
当前线程是:Thread-0对应的编号是:5
当前线程是:Thread-0对应的编号是:6
当前线程是:Thread-0对应的编号是:7
当前线程是:Thread-0对应的编号是:8
当前线程是:Thread-0对应的编号是:9
当前线程是:Thread-0对应的编号是:10
当前线程是:Thread-2对应的编号是:1
当前线程是:Thread-2对应的编号是:2
当前线程是:Thread-2对应的编号是:3
当前线程是:Thread-2对应的编号是:4
当前线程是:Thread-2对应的编号是:5
当前线程是:Thread-2对应的编号是:6
当前线程是:Thread-2对应的编号是:7
当前线程是:Thread-2对应的编号是:8
当前线程是:Thread-2对应的编号是:9
当前线程是:Thread-2对应的编号是:10
当前线程是:Thread-1对应的编号是:1
当前线程是:Thread-1对应的编号是:2
当前线程是:Thread-1对应的编号是:3
当前线程是:Thread-1对应的编号是:4
当前线程是:Thread-1对应的编号是:5
当前线程是:Thread-1对应的编号是:6
当前线程是:Thread-1对应的编号是:7
当前线程是:Thread-1对应的编号是:8
当前线程是:Thread-1对应的编号是:9
当前线程是:Thread-1对应的编号是:10
当前线程是:Thread-3对应的编号是:1
当前线程是:Thread-3对应的编号是:2
当前线程是:Thread-3对应的编号是:3
当前线程是:Thread-3对应的编号是:4
当前线程是:Thread-3对应的编号是:5
当前线程是:Thread-3对应的编号是:6
当前线程是:Thread-3对应的编号是:7
当前线程是:Thread-3对应的编号是:8
当前线程是:Thread-3对应的编号是:9
当前线程是:Thread-3对应的编号是:10

可以看出每个线程都产生出了10个数字。他们互不影响,线程运行的顺序可能有会不同,但是每个都是独立的,以当前线程作为键,值作为value,每个数字产生器都生成了独立的数字,达到了线程独立的效果。

四:总结

   本篇博文介绍了ThreadLocal,主要从结构、源码角度分析了它的api,对于向外暴露出来的get/set/remove方法都进行了分析,关于其实际使用,用两个简单的例子进行了表现,希望从本篇博文中能更进一步的学习和了解到ThreadLocal,对于我们在多线程的学习过程中,适当的使用ThreadLocal。

 

posted @ 2018-02-24 18:17  Yrion  阅读(955)  评论(0编辑  收藏  举报