理解和使用ThreadLocal类
一、从数据结构入手
下图为ThreadLocal的内部结构图
从上面的机构图,可以窥见ThreadLocal的核心机制:
- 每个Thread线程内部都有一个Map;
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值;
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
二、深入解析ThreadLocal
ThreadLocal类提供如下几个核心方法:
public T get()
public void set(T value)
public void remove()
protected T initialValue() { }
- get()方法用于获取当前线程的变量副本
- set()方法用户保存当前线程的变量副本
- initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,用于为当前线程初始变量副本值
- remove()方法移除当前线程的副本变量
get()方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
第一句取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对Entry节点,注意这里获取Entry传进去的是this,而不是当前线程t。
如果获取成功,则返回value值。
如果map为空,则调用setInitialValue方法返回value。
我们对上面的每一句仔细分析:
首先看一下getMap方法中做了什么:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可能大家没有想到的是,在getMap中,是调用当前线程t,返回当前线程t中的一个成员变量threadLocals。
那么我们继续取Thread类中去看一下成员变量threadLocals是什么:
public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,我们继续去看ThreadLocalMap的实现:
static class ThreadLocalMap { /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
可以看到ThreadLocalMap的内部类Entry继承了WeakReference,并且使用ThreadLocal作为键值。
然后再继续看setInitialValue()方法的具体实现:
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
很容易了解,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
至此,我们可以明白ThreadLocal是如何为每个线程创建变量的副本的:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,key为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
set()方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取当前线程的成员变量map,若map非空,则重新将ThreadLocal和新的value副本放入到map中。
若map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
remove()方法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
将当前ThreadLocal对应的value副本从map中移除。
三、ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是Threadlocal对象,这点被Entry的构造方法已经限定死了。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
ThreadLocalMap的成员变量:
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
四、Hash冲突怎么解决
和HashMap的最大不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始Key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其它Key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
} /**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发生冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存放一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大地增加Hash冲突的可能。
五、ThreadLocalMap的问题
由于ThreadLocalMap的Key是弱引用,而Value是强引用。这就导致一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时,完成后再调用remove()方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的get方法之后,没有显式地调用remove()方法,就有可能发生内存泄漏,所以养成良好的变成习惯十分重要,使用完ThreadLocal之后,记得调用remove()方法。如:
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.get();
// 其它业务逻辑
} finally {
threadLocal.remove();
}
六、ThreadLocal的应用场景
最常见的ThreadLocal的使用场景为用来解决数据库连接、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相当于线程的私有变量。显然,其它线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。
ThreadLocal的应用场合,我觉得最适合的是对同一个线程调用的多个方法中共享了某一个变量(如上面的session),这个变量需要传递到多个方法中(如上的session在service层和dao层的很多方法中都要用到),这样传来传去太麻烦了,这时可以采用ThreadLocal。
这里还有一个在Java Web中使用ThreadLocal的例子可供学习:《优雅的使用ThreadLocal》
七、总结
ThreadLocal并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果,实际上自己的方法中new出来变量也能达到类似的效果。ThreadLocal跟线程安全基本不搭边,绑定上去的实例也不是多线程共用的,而是每个线程new一份,这个实例肯定不是共用的,如果共用了,那就会引发线程安全问题。ThreadLocal最大的用处是用来把实例变量共享成全局变量,在线程的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocal是解决了线程安全问题,其实是望文生义,两者不是同类问题。
补充:ThreadLocal类型变量为何声明为静态static?
Java中每个线程都有与之关联的Thread对象,Thread对象中有一个ThreadLocal.ThreadLocalMap类型的成员变量,该变量是一个Hash表,所以每个线程都单独维护这样一个Hash表,当ThreadLocal类型对象调用 set()方法时,这个set()方法会使用当前线程维护的Hash表,把自己(ThreadLocal)作为Key,相应的值作为value插入到Hash表中。由于每个线程维护的Hash表是独立的,因此在不同的Hash表中,key值即使相同也是没问题的。
如果把ThreadLocal对象声明为非静态的,则当包含ThreadLocal类声明的类去产生一个实例时,都会产生一个ThreadLocal的新对象,这是毫无意义的,只是增加了内存消耗。
理解和使用ThreadLocal类的更多相关文章
- ThreadLocal类的理解
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各 ...
- 理解ThreadLocal类
1 ThreadLocal是什么 早在JDK 1.2的版本号中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路. 使用这个工具类能够 ...
- 深入理解java:2.4. 线程本地变量 java.lang.ThreadLocal类
ThreadLocal,很多人都叫它做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多. 可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那样每个线程可以访问自己内 ...
- 谈谈对ThreadLocal类的理解
源码中对于ThreadLocal类的解释是: /** * This class provides thread-local variables. These variables differ from ...
- ThreadLocal类详解:原理、源码、用法
以下是本文目录: 1.从数据库连接探究 ThreadLocal 2.剖析 ThreadLocal 源码 3. ThreadLocal 应用场景 4. 通过面试题理解 ThreadLocal 1.从数据 ...
- 用ThreadLocal类实现线程安全的正确姿势
大家通常知道,ThreadLocal类可以帮助我们实现线程的安全性,这个类能使线程中的某个值与保存值的对象关联起来.ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量 ...
- 2015年11月26日 Java基础系列(三)ThreadLocal类初级学习
序,ThreadLocal类是为了解决多线程的安全问题.线程安全的意思也就是说每个线程操作自己的变量,不要对其他线程的值造成影响. 在很多情况下,ThreadLocal比直接使用synchronize ...
- Java多线程——ThreadLocal类
一.概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名 ...
- Java并发编程之ThreadLocal类
ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当 ...
随机推荐
- JAVA核心技术I---JAVA基础知识(类的继承)
一:基本概念同C++一致 二:继承演示 .将共同点提取出来,即形成了父类/基类/超类 –Parent class/Base class/Super class .而其他类则自动成为子类/派生类 –Ch ...
- jest-babel报错:Requires Babel "^7.0.0-0", but was loaded with "6.26.3"
解决方法: yarn remove jest babel-jest babel-core @babel/core yarn add --dev jest babel-jest babel-core@^ ...
- Web API中的内容协商
一.内容协商的概念 HTTP规范将内容协商定义为“当有多个格式可用时为给定响应选择最佳格式的过程”.HTTP中内容协商的主要机制是这些请求标头: Accept:响应可接受哪些媒体类型,例如“appli ...
- blackeye部署
部署: apt-get install php apt-get install libapache2-mod-php apt-get install git git clone https://git ...
- Tips and Tricks for Debugging in chrome
Tips and Tricks for Debugging in chrome Pretty print On sources panel ,clicking on the {} on the bot ...
- webservice 项目中遇到的问题
redshift database 连接异常 解决方案 url 修改添加参数如下 jdbc:redshift://hostname:5439/dbname?ssl=true&sslfactor ...
- 爬虫基础02-day24
写在前面 上课第24天,打卡: 努力不必让全世界知道: s16/17爬虫2 内容回顾: 1. Http协议 Http协议:GET / http1.1/r/n...../r/r/r/na=1 TCP协议 ...
- 2.4 if-else
if-else 想一想:在使用if的时候,它只能做到满足条件时要做的事情.那万一需要在不满足条件的时候,做某些事,该怎么办呢? 答:使用 if-else <1>if-else的使用格式 i ...
- Ubuntu18.04下Python Web环境搭建
Python3的安装与卸载 pip, pip3的安装 Django2.x的安装 mysql的安装及命令行工具mycli的安装 virtualenv及virtualenvwrapper的安装 IPyth ...
- Important persons in deep learning
Geoffrey Hinton Pieter Abbeel Ian Goodfellow, GAN之父 2019.3 任 Apple Director Andrej Karpathy. 视觉大牛,李飞 ...