ThreadLocal源码解析
本篇博客的目录:
一:ThreadLocal的简介
二:ThreadLocal源码分析
三:ThreadLocal实例
四:总结
一:ThreadLocal的简介
1.1:简单解释
ThrealLocal望文生义,简单解释就是线程的本地变量。我们来看一下jdk对它的定义:该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。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处理
其中关于弱引用: 具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
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。