线程本地变量,每一个线程保存变量的副本,对副本的改动,对其余的线程而言是透明的(即隔离的)java
使用方式也比较简单,经常使用的三个方法web
// 设置当前线程的线程局部变量的值 void set(Object value); // 该方法返回当前线程所对应的线程局部变量 public Object get(); // 将当前线程局部变量的值删除 public void remove();
下面给个实例,来瞅一下,这个东西通常的使用姿式。一般要获取线程变量, 直接调用 ParamsHolder.get()
算法
public class ParamsHolder { private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>(); @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)
直接看源码中的两个方法, 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; }
逻辑比较清晰多线程
threadLocals
属性ThreadLocalMap
threadLocals属性ide
这个属性的解释以下,简单来说,这个里面的变量都是线程独享的,彻底由线程本身hold住学习
ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.this
接下来须要了解的就是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次方内尽量均匀分布,减小冲突的几率
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()
方法进行清楚
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); } }
想想,为何这种方式是线程安全的呢?
ThreadLocal 线程本地变量,每一个线程保存变量的副本,对副本的改动,对其余的线程而言是透明的(即隔离的)
三个经常使用的方法
// 设置当前线程的线程局部变量的值 void set(Object value); // 该方法返回当前线程所对应的线程局部变量 public Object get(); // 将当前线程局部变量的值删除 public void remove();
利用了HashMap的设计理念,一个map中存储Thread->线程变量的映射关系, 所以线程变量在多线程之间是隔离的
一般建议是线程执行完毕以后,主动去失效掉ThreadLocal中的变量,以防止线程复用致使变量被乱用了
尽信书则不如,已上内容,纯属一家之言,因本人能力通常,见识有限,若有问题,请不吝指正,感激