ThreadLocal
其实可以理解成每个线程独有的一块区域,线程间不能相互影响的一块区域(其中父子线程可以通过InheritableThreadLocal
实现子线程读取父线程ThreadLocal
的数据)。
于是乎,我搜索了下项目中使用到ThreadLocal
的地方。
说说用到的几个场景:
-
对象隔离(线程需要一个独享的对象,例如
SimpleDateFormat
)-
单线程
public class Test { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { simpleThread(); } private static void simpleThread() { new Thread(){ @Override public void run() { while(true) { //因为只有一个线程所以就类似是睡眠2000毫ms this.join(2000); System.out.println(this.getName() + ":" + sdf.parse("2013-05-24 06:02:20")); } } }.start(); } } -------------------------------------------------------------- Thread-0:Fri May 24 06:02:20 CST 2013 Thread-0:Fri May 24 06:02:20 CST 2013 Thread-0:Fri May 24 06:02:20 CST 2013 Thread-0:Fri May 24 06:02:20 CST 2013
这里没有问题,因为单线程运行,不会发生线程安全问题。这里要注意的是
SimpleDateFormat
是线程不安全的对象,当它在格式化时间的时候里边会操作内部的Calendar对象(没有线程安全处理)所以导致在多线程中会出错。 -
多线程
private static void manyThread() { for(int i = 0; i < 3; i++){ new Thread(){ @Override public void run() { while(true) { //这里因为是多线程是让其他线程执行,这里就是为了更好的复现SimpleDateFormat线程不安全操作 this.join(2000); System.out.println(this.getName() + ":" + sdf.parse("2013-05-24 06:02:20")); } } }.start(); } } -------------------------------------------------------------- Thread-2:Tue Feb 20 18:58:40 CST 20132024 Thread-1:Tue Feb 20 18:58:40 CST 20132024 Thread-0:Fri May 24 06:02:20 CST 2013 Thread-1:Mon May 24 06:02:20 CST 1
调用
SimpleDataFormat
的parse()方法会先调用Calendar.clear()
,然后调用Calendar.add()
,如果一个线程先调用了add()
然后另一个线程又调用了clear()
,这时候parse()
方法解析的时间就不对了。这里有人会说那定义成局部变量就可以了呗。是的,不是为了减少
new
对象的开支嘛。哈哈哈哈哈这下
ThreadLocal
来了,我们只要保证每个线程有自己的SimpleDataFormat
并且只用自己的SimpleDataFormat
就可以保证不出错了。那这个场景和ThreadLocal
特性一毛一样啊。 -
多线程使用
ThreadLocal
解决线程安全public class Test { //每个线程都会有一个自己的SimpleDateFormat private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static Date parse(String strDate) throws ParseException { return threadLocal.get().parse(strDate); } public static void main(String[] args) { simpleThread(); } private static void simpleThread() { for(int i = 0; i < 3; i++){ new Thread(){ @Override public void run() { while(true) { this.join(2000); //这里用的parse方法是从ThreadLocal取出来的SimpleDateFormat System.out.println(this.getName() + ":" + Test.parse("2013-05-24 06:02:20")); } } }.start(); } } } -------------------------------------------------------------- Thread-1:Fri May 24 06:02:20 CST 2013 Thread-2:Fri May 24 06:02:20 CST 2013 Thread-0:Fri May 24 06:02:20 CST 2013 Thread-1:Fri May 24 06:02:20 CST 2013
-
-
对象传递
线程需要保存全局变量,可以让不同的方法直接使用,而不需要让数据作为参数层层传递。**强调的是同一个请求内(同一个线程内)不同方法间的共享。**当然Map也可以存储上述业务信息。多线程同时工作时,需要保证线程安全。例如,采用静态
ConcurrentHashMap
变量,将线程ID作为key,业务数据作为Value保存,可以做到线程间隔离。可以实现的方式有很多。我们这里简单写个存储用户信息的实现。
public class RpcFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,ServletException { //从request信息中读取session信息 HttpSession session = ((HttpServletRequest) request).getSession(true); //取得缓存中的用户信息 Object sessionUser = session.getAttribute(RpcHolder.DEFAULT_USER_INFO); if (sessionUser != null) { //如果存在则先清除 if (RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) { RpcHolder.unbindResource(RpcHolder.DEFAULT_USER_INFO); } //将用户信息加入缓存中 RpcHolder.bindResource(RpcHolder.DEFAULT_USER_INFO, sessionUser); } //执行过滤操作 chain.doFilter(request, response); Object serviceUser = RpcHolder.getResource(RpcHolder.DEFAULT_USER_INFO); if (RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) { RpcHolder.unbindResource(RpcHolder.DEFAULT_USER_INFO); } //存入session session.setAttribute(RpcHolder.DEFAULT_USER_INFO, serviceUser); //这里一定要清除,不然会发生内存泄漏 RpcHolder.remove(); } }
public abstract class RpcHolder { // 该变量用于存放用户信息 public static final String DEFAULT_USER_INFO = "DEFAULT_USER_INFO"; //NamedThreadLocal是ThreadLocal子类,只是多个了name的属性。可以通过name获取 private static final ThreadLocal resources = new NamedThreadLocal("phprpc resources"); //判断是否为空 public static boolean hasResource(String key) { Object value = doGetResource(key); return value != null; } //获取用户信息 public static Object getResource(String key) { Object value = doGetResource(key); return value; } private static Object doGetResource(String actualKey) { Map map = (Map) resources.get(); if (map == null) { return null; } Object value = map.get(actualKey); return value; } //绑定用户信息 public static void bindResource(String key, Object value) throws IllegalStateException { Map map = (Map) resources.get(); if (map == null) { map = new HashMap(); resources.set(map); } map.put(key, value) } //清空用户信息 public static Object unbindResource(String key) throws IllegalStateException { Map map = (Map) resources.get(); if (map == null) { return null; } Object value = map.remove(actualKey); if (map.isEmpty()) { resources.set(null); } return value; } public static void remove(){ resources.remove(); } }
//获取用户信息工具类这个就是从ThreadLocal中获取 public class UserUtils { public static UserInfo getLoginUser() { if (!RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) { throw new InfoException("登录已超时,请重新登录!"); } return (UserInfo) RpcHolder.getResource(RpcHolder.DEFAULT_USER_INFO); } }
简单说下思路,在每次请求过来我们会拦截请求把用户信息获取到后放到当前线程的
ThreadLocal
中,然后再处理业务的请求中就不需要从Session
中获取,直接调用UserUtils
方法从ThreadLocal
获取即可。无论在Controller
还是Service
都可以获取不需要在传递用户对象。 -
分布式系统链路追踪
这里不过多赘述,
MDC
实现简单好用。底层也是用ThreadLocal
实现。public class BasicMDCAdapter implements MDCAdapter { private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() { }
ThreadLocal
底层实现:
先说说怎么做到每个线程只能读取到自己线程内的数据,数据隔离是怎么做到的。
public class TestSession {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
threadLocal.set("a");
threadLocal.get();
}
}
看到get()/set()
方法都是通过ThreadLocalMap map = getMap(t);
获取到ThreadLocalMap
然后ThreadLocalMap.Entry e = map.getEntry(this);
然后操作的。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
主要就是ThreadLocalMap
我们需要关注一下,而ThreadLocalMap
呢是当前线程Thread
一个叫threadLocals
的变量中获取的。这里我们基本上可以找到ThreadLocal
数据隔离的真相了,每个线程Thread
都维护了自己的threadLocals
变量,所以在每个线程创建ThreadLocal
的时候,实际上数据是存在自己线程Thread
的threadLocals
变量里面的,别人没办法拿到,从而实现了隔离。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
}
ThreadLocalMap
底层结构是Entry
数组,Entry
则是类似Map
的key(弱引用)-val
键值对。
结构大致是这样的:(各自线程有个threadlocals
是个数组结构)
这里可能会有疑惑,ThreadLocal
不是只能保存一个Entry
么?重复就会被覆盖么?为什么还需要用数组?
public class TestSession {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();
public static void main(String[] args) {
threadLocal1.set("a");
threadLocal2.set("a");
}
}
这里要清楚一个线程只有一个threadlocals
,但是可以有多个ThreadLocal
所以每个ThreadLocal
会占用一个数组节点。
那么又有人会问hash
碰撞了怎么办?Map
是用数组+链表解决,那这个怎么解决?
看看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);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算下标
int i = key.threadLocalHashCode & (len-1);
/**
* 指定下标被占用
* for会从当前 i 下标开始循环遍历数组,直到数组单元为空退出循环
* 这里也就是hash冲突之后会向后继续找有空位则入位或者是有相同的进行覆盖(e = tab[i = nextIndex(i, len))
*/
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//指定下标的threadlocal和set的一致,直接覆盖
if (k == key) {
e.value = value;
return;
}
//表示threadLocal被gc回收(因为entry对threadLocal是弱引用),进行entry清理工作
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//当前下标没有被占用,直接在指定下标赋值
tab[i] = new Entry(key, value);
int sz = ++size;
//扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
以下就是梳理的set()
执行流程:
具体清理流程去看这篇博客,图文分析真的很全面。
https://blog.51cto.com/u_7592962/2543172
- 遍历当前
key
值对应的桶中Entry
数据为空,这说明散列数组这里没有数据冲突,跳出for
循环,直接set
数据到对应的桶中跳到3.1 - 如果
key
值对应的桶中Entry
数据不为空
2.1 如果k = key
,说明当前set
操作是一个替换操作,做替换逻辑,直接返回
2.2 如果key = null
,说明当前桶位置的Entry
是过期数据,执行replaceStaleEntry()
方法(核心方法),然后返回 for
循环执行中在后迭代的过程中遇到了entry
为null
的情况则跳出循环
3.1 在Entry
为null
的桶中创建一个新的Entry
对象
3.2 执行++size
操作- 调用
cleanSomeSlots()
做一次启发式清理工作,清理散列数组中Entry
的key
过期的数据
4.1 如果清理工作完成后,未清理到任何数据,且size
超过了阈值(数组长度的2/3),进行rehash()
操作
4.2rehash()
中会先进行一轮探测式清理,清理过期key
,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑
知道了怎么赋值那么取值就更简单了。
private Entry getEntry(ThreadLocal<?> key) {
//计算下标,找到并且threadlocal也相等直接返回
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//否则往后找
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//玄幻往后找,找到返回。
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//entry清理工作
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
内存泄漏:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
因为继承WeakReference
弱引用所以key
是弱引用threadlocal
弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal
在没有外部强引用时,发生GC
时会被回收,如果创建ThreadLocal
的线程一直持续运行,那么这个Entry
对象中的value
就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal
设定的value
值被持有,导致内存泄露。
那为什么不设置成强引用呢?
当ThreadLocalMap
的key
为强引用回收ThreadLocal
时,因为ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry内存泄漏。 譬如 设置:ThreadLocal = null
以后,应该会被回收的,但实际情况是ThreadLocalMap
还有一个强引用,导致无法回收也会导致内存泄漏。
现在ThreadLocalMap
的key
为弱引用,设置:ThreadLocal = null
以后ThreadLocalMap
是弱引用所以就可以回收。其实道理和为什么key
为弱引用一致。
那怎么解决?
在代码的最后使用remove
就好了,我们只要记得在使用的最后用remove
把值清空就好了。