Java并发学习之ThreadLocal使用及原理介绍
2017-11-27 22:16
489 查看
ThreadLocal使用及原理介绍
线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的(即隔离的)1. 使用姿势一览
使用方式也比较简单,常用的三个方法// 设置当前线程的线程局部变量的值 void set(Object value); // 该方法返回当前线程所对应的线程局部变量 public Object get(); // 将当前线程局部变量的值删除 public void remove();
下面给个实例,来瞅一下,这个东西一般的使用姿势。通常要获取线程变量, 直接调用
ParamsHolder.get()
public class ParamsHolder { private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLo 4000 cal<>(); @ToString @Getter @Setter public static class Params { private String mk; } public static void setParams(Params params) { PARAMS_INFO.set(params); } public static void clear() { PARAMS_INFO.remove(); } public static Params get() { return PARAMS_INFO.get(); } public static void main(String[] args) { Thread child = new Thread(new Runnable() { @Override public void run() { System.out.println("child thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("thread")); System.out.println("child thread final: " + ParamsHolder.get()); } }); child.start(); System.out.println("main thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("main")); System.out.println("main thread final: " + ParamsHolder.get()); } }
输出结果
child thread initial: null main thread initial: null child thread final: ParamsHolder.Params(mk=thread) main thread final: ParamsHolder.Params(mk=main)
2. 实现原理探究
直接看源码中的两个方法, get/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); } 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(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
主要以set方法进行讲解
逻辑比较清晰获取当前线程对象
获取到线程对象中的
threadLocals属性
将value塞入
ThreadLocalMap
threadLocals属性
这个属性的解释如下,简单来讲,这个里面的变量都是线程独享的,完全由线程自己hold住
ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
接下来需要了解的就是
ThreadLocalMap这个对象的内部构造了,里面的有个table对象,维护了一个Entry的数组
table,
Entry的key为
ThreadLocal对象,value为具体的值。
//ThreadLocalMap.java static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; 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(); }
聚焦在
int i = key.threadLocalHashCode & (table.length - 1);这一行,这个就是获取Entry对象在
table中索引值的主要逻辑,主要利用当前线程的hashCode值
假设出现两个不同的线程,这个code值一样,会如何?
这种类似hash碰撞的场景,会调用
nextIndex来获取下一个位置
针对上面的逻辑,有点有必要继续研究下,
hashCode的计算方式, 为什么要和数组的长度进行与计算
作为ThreadLocal实例的变量只有 threadLocalHashCode 这一个,
nextHashCode和
HASH_INCREMENT是ThreadLocal类的静态变量,实际上
HASH_INCREMENT是一个常量,表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量,而nextHashCode 的表示了即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值
所有ThreadLocal对象共享一个AtomicInteger对象nextHashCode用于计算hashcode,一个新对象产生时它的hashcode就确定了,算法是从0开始,以HASH_INCREMENT = 0x61c88647为间隔递增,这是ThreadLocal唯一需要同步的地方。根据hashcode定位桶的算法是将其与数组长度-1进行与操作
ThreadLocalMap的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,上述算法中使用0x61c88647可以让hash的结果在2的n次方内尽可能均匀分布,减少冲突的概率
3. 线程池中使用ThreadLocal
的注意事项
这里主要的一个问题是线程复用时, 如果不清除掉ThreadLocal 中的值,就会有可怕的事情发生, 先简单的演示一下private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(0); } }; static class Task implements Runnable { @Override public void run() { AtomicInteger s = threadLocal.get(); int initial = s.getAndIncrement(); // 期望初始为0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); }
输出结果
0 0 1
说好的线程变量,这里居然没有按照我们预期的来玩,主要原因就是线程复用了,而线程中的局部变量没有清零,导致下一个使用这个线程的时候,这些局部变量也带过来,导致没有按照我们的预期使用
这个最可能导致的一个超级严重的问题,就是web应用中的用户串掉的问题,如果我们将每个用户的信息保存在
ThreadLocal中, 如果出现线程复用了,那么问题就会导致明明是张三用户,结果登录显示的是李四的帐号,这下就真的呵呵了
因此,强烈推荐,对于线程变量,一但不用了,就显示的调用
remove()方法进行清楚
4. 经典case
SimpleDataFormate是一个非线程安全的类,可以使用 ThreadLocal 完成的线程安全的使用
public class ThreadLocalDateFormat { static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String date2String(Date date) { return sdf.get().format(date); } public static Date string2Date(String str) throws ParseException { return sdf.get().parse(str); } }
想一想,为什么这种方式是线程安全的呢?
II. 小结
1. 一句话介绍
ThreadLocal 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的(即隔离的)2. 常用方法
三个常用的方法// 设置当前线程的线程局部变量的值 void set(Object value); // 该方法返回当前线程所对应的线程局部变量 public Object get(); // 将当前线程局部变量的值删除 public void remove();
3. 实现原理
利用了HashMap的设计理念,一个map中存储Thread->线程变量的映射关系, 因此线程变量在多线程之间是隔离的4. 注意事项
通常建议是线程执行完毕之后,主动去失效掉ThreadLocal中的变量,以防止线程复用导致变量被乱用了III. 其他
声明
尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如有问题,请不吝指正,感激扫描关注,不定时分享各种java学习笔记
相关文章推荐
- java并发编程学习: ThreadLocal使用及原理
- java并发(3)ThreadLocal的使用及实现原理(实现原理)
- java并发(2)ThreadLocal的使用及实现原理(使用)
- java并发编程学习: 阻塞队列 使用 及 实现原理
- Java 并发编程深入学习——CopyOnWrite容器使用和原理分析
- Java 并发编程深入学习——ThreadLocal 原理分析
- [Java并发包学习二]Executors介绍
- dubbo学习过程、使用经验分享及实现原理简单介绍
- Dubbo学习过程、使用经验分享及实现原理简单介绍
- Java高精度学习第三弹——ACM中使用JAVA的详细介绍
- Java反射学习总结四(动态代理使用实例和内部原理解析)
- java并发库中的LockSupport介绍及使用
- [Java并发包学习七]解密ThreadLocal
- dubbo学习过程、使用经验分享及实现原理简单介绍
- 漫谈并发编程(六):java中一些经常使用的并发构件的介绍
- [Java并发包学习六]Semaphore介绍
- Java并发学习之九——使用本地线程变量
- Java并发学习之十五——使用读写锁同步数据访问
- [Java并发包学习七]解密ThreadLocal