#前言
面试多线程好像必不可少的一个知识点
是什么?有什么用?应用场景?
ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型,不允许外部修改。 它和线程同步是相反的作用,以实现每个线程内的变量都是独自分开的,互不影响。
ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程可以创建一个或者多个ThreadLocal,ThreadLocal本身不是什么对象的拷贝或副本。核心在于Thread类本身就包含一个定义了的ThreadLocal.ThreadLocalMap类型的threadLocals。
注意
ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set()
到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
#线程局部变量
使用场景
在多线程环境下,之所以会有并发问题,就是因为不同的线程会同时访问同一个共享变量,例如下面的形式,模拟多线程下局部变量的修改问题:
public class MultiThreadDemo {
public static class Number {
private int value = 0;
public void increase() throws InterruptedException {
value = 10;
Thread.sleep(10);
System.out.println("increase value: " + value);
}
public void decrease() throws InterruptedException {
value = -10;
Thread.sleep(10);
System.out.println("decrease value: " + value);
}
}
//执行多线程对局部变量进行修改操作
public static void main(String[] args) throws InterruptedException {
final Number number = new Number();
Thread increaseThread = new Thread(new Runnable() {
@Override
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread decreaseThread = new Thread(new Runnable() {
@Override
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
increaseThread.start();
decreaseThread.start();
}
}
increase 线程和 decrease 线程会操作同一个 number 中 value,那么输出的结果是不可预测的,因为当前线程修改变量之后但是还没输出的时候,变量有可能被另外一个线程修改
在多线程中如何保证数据的原子性呢,当然我们可以在方法上使用同步synchronized,这样就成为了同步代码,显然在性能上有影响,我们要
#ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
-
void set(Object value)
-
public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 1.5 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。 -
protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。 -
T get()
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中定义了一个ThreadLocalMap,每一个Thread中都有一个该类型的变量——threadLocals——用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
通过源码可以看出每个
public void set(T value) {
//所有设置的数据都以当前主线程的hashcode为标识,所以只要线程不同它的变量threadLocals就不会相同
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);// 获取当前主线程的threadLocals,它是一个map类型,所以可以存放多个值,key是以当前变量ThreadLocal类型为key值加以区分
if (map != null)
map.set(this, value);
else
createMap(t, value);//如果发现threadLocals是初始值null则把主线程的变量threadLocals新建一个,并把当ThreadLocal变量-值作为键值对保存进去
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前主线程的threadLocals
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取当前ThreadLocal变量为key的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
参考源码资料https://www.cnblogs.com/dolphin0520/p/3920407.html
#应用场景
##hibernate中的使用
下面来看一个hibernate中典型的ThreadLocal的应用:
每次获取的都是自己的session,而不是共享的session
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。
显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。
试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了,不知道除了这些还有什么其他的好处。
##日志记录
计算当前线程的方法运行时间
/**
* zjcjava@163.com
*/
/**
* 系统日志拦截器:需要在springMVC中配置
* @author zjcjava@163.com
* @version 2017-8-19
*/
public class SysLogInterceptor extends BaseService1 implements HandlerInterceptor {
private static final ThreadLocal<Long> startTimeThreadLocal =
new NamedThreadLocal<Long>("ThreadLocal StartTime");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
long beginTime = System.currentTimeMillis();//1、开始时间
startTimeThreadLocal.set(beginTime); //线程绑定变量(该数据只有当前请求的线程可见)
if (logger.isDebugEnabled()){
logger.debug("开始计时: {} URI: {}", new SimpleDateFormat("hh:mm:ss.SSS")
.format(beginTime), request.getRequestURI());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null){
logger.info("ViewName: " + modelAndView.getViewName());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception exception) throws Exception {
Long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间)
if(beginTime==null){
beginTime=System.currentTimeMillis();
}
long endTime = System.currentTimeMillis(); //2、结束时间
double executeTime =(double)(endTime - beginTime)/1000;//秒
SysLog log=sysLogThreadLocal.get();
log.setExecuteTime(executeTime);
log.setType(exception == null ?SysLog.TYPE_ACCESS :SysLog.TYPE_EXCEPTION);
// 保存日志
SysLogUtils.saveLog(log, handler, exception);
//删除线程变量中的数据,防止内存泄漏
startTimeThreadLocal.remove();
}
}
}
在拦截器中每个线程都可以在进入方法前保存当前时间戳,方法运行完后计算总共的运行时长。其他线程是无法修改该开始时间的。
#实现原理
ThreadLocal
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
看源码
public T get() {
Thread t = Thread.currentThread();//当前线程
ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}
//设置变量的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//默认以当前线程的实例作为KEY值
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中
@param t the current thread
@param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//删除当前线程中ThreadLocalMap对应的ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
看 set(T value) 方法Thread t = Thread.currentThread()以当前线程的实例作为map中的KEY,
总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分key,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象放到map中。
##ThreadLocalMap
开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 – 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。如下图所示:
ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。
实现
我们知道 Map 是一种 key-value 形式的数据结构,所以在散列数组中存储的元素也是 key-value 的形式。ThreadLocalMap 使用 Entry 类来存储数据,下面是该类的定义:
//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
/**
* 初始化容量为16,以为对其扩充也必须是2的指数
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
*/
private Entry[] table;
///....其他的方法和操作都和map的类似
可以看到extends WeakReference使用了弱引用,这也导致了后续会产生内存泄漏问题的原因。
#ThreadLocal内存泄漏问题(参考其他博文)
在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据【请参考本文使用场景:日志记录】。
内存泄漏的样例,参考
ThreadLocal 内存泄露的实例分析
http://blog.csdn.net/lhqj1992/article/details/52451136
#参考文章
参考文章
http://www.iteye.com/topic/103804
http://www.jianshu.com/p/529c03d9b67e
http://stackoverflow.com/questions/38994306/what-is-the-meaning-of-0x61c88647-constant-in-threadlocal-java
http://jerrypeng.me/2013/06/thread-local-and-magical-0x61c88647/
ThreadLocal是否会引发内存泄露的分析(转)
[Java并发包学习七]解密ThreadLocal
数据结构与算法分析: C语法描述