博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ThreadLocal源码解析
阅读量:4321 次
发布时间:2019-06-06

本文共 12243 字,大约阅读时间需要 40 分钟。

本篇博客的目录:

一: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( value)
将此线程局部变量的当前线程副本中的值设置为指定值。

二:ThreadLocal的源码分析

2.1:误解读之处

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

 

 

2.2:关于Entry对象

static class Entry extends WeakReference
{
//用虚引用封装的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
seqNum = new ThreadLocal
() { 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。

 

转载于:https://www.cnblogs.com/wyq178/p/8414719.html

你可能感兴趣的文章
ORACLE wm_concat自定义
查看>>
[Zend PHP5 Cerification] Lectures -- 6. Database and SQL
查看>>
[Drupal] Using the Administrator theme whenever you want.
查看>>
【Hibernate框架】关联映射(一对一关联映射)
查看>>
【算法】大数乘法
查看>>
WPF解析PPT为图片
查看>>
JavaScrict中的断言调试
查看>>
密码服务
查看>>
结构体在内存中的存储
查看>>
冲刺阶段—个人工作总结01
查看>>
基于Python的Webservice开发(二)-如何用Spyne开发Webservice
查看>>
PowerDesigner修改设计图中文字的字体大小等样式
查看>>
Python list和 np.Array 的转换关系
查看>>
jenkins忘记密码如何处理?
查看>>
布尔操作符-逻辑或(||)
查看>>
vim的列编辑操作
查看>>
Linux驱动学习 —— 在/sys下面创建目录示例
查看>>
Linux下安装Android的adb驱动-解决不能识别的问题
查看>>
Why is the size of an empty class not zero in C++?
查看>>
海亮SC
查看>>