ThreadLocal介绍
ThreadLocal,意为线程的局部变量,是在线程本地存了一个变量的副本。只有当前线程可以访问。它的目的是为了在线程之间共享某个变量,而不会发生冲突。就好比有100个人需要填一张表,按照同步机制的做法就是保证这支笔同一时间只有一个人使用,而ThreadLocal则是采用另一种方式:每个人都发了一支笔。相比同步机制,这是一种“空间换时间”的做法。
在同步机制中,使用了synchronized和volatile等关键字实现了这一个目的,但实际上这样的实现是比较耗时的,相比ThreadLocal,这样做属于“时间换空间”。
ThreadLocal使用&使用场景
ThreadLocal的使用场景:
每条线程都需要存取一个同名变量,但每条线程中该变量的值不完全相同。
使用ThreadLocal使用主要有以下方法:
public void set(T value);
:为当前线程设置一个value值,即只有当前线程可以访问的值public T get();
:获取set的值public void remove();
:删除线程当前threadlocal的值,无法恢复
这里假设要使用一个SimpleDateFormat
对象来实现字符串类型日期的格式化,但是由于SimpleDateFormat.parse()
方法并不是线程安全的,所以需要保证同一时间只有一个线程使用SimpleDateFormat
,可以使用synchronized,也可以使用ThreadLocal。这里演示ThreadLocal的方式。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by makersy on 2019
*/
public class ThreadLocalDomo {
// static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
public static class ParseDate implements Runnable {
int i = 0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t = tl.get().parse("2019-01-01 19:10:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; ++i) {
es.submit(new ParseDate(i));
}
es.shutdown();
}
}
这里首先判断ThreadLocal有没有被设置,没有就进行set,然后使用Thread的本地变量来进行日期的格式化。
源码分析
内部结构
每个线程内部都定义了一个ThreadLocalMap,初始为null。ThreadLocalMap是ThreadLocal的静态内部类,内部定义了一个类似map的键值对结构,称为Entry。
对ThreadLocal的弱引用和其对应的对象以键值对的形式存为Entry,然后存放在Entry数组里,注意这个Entry数组是定义在ThreadLocalMap类中。
之所以要定义成Entry数组,是因为一个线程可能会设置多个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
,也就是对ThreadLocal的弱引用,注意它并不是ThreadLocal本身,只是一个引用。为什么要继承WeakReference,这个后面详细说明。
线程、ThreadLocal、ThreadLocalMap、Entry之间是个什么关系,我花了好久才理解,这里我画了一张图来表示:
ThreadLocal总体结构:
ThreadLocal的set、get、remove方法
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 [email protected] #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); //当前线程的ThreadLocalMap
if (map != null) //存在ThreadLocalMap
map.set(this, value); //在entry数组中添加或者覆盖设置的值
else
createMap(t, value); //不存在map,新建并赋值
}
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 [email protected] #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存在
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果map中存在调用此方法的ThreadLocal对象
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result; //正常返回
}
}
return setInitialValue(); //上述任意一个条件不满足的话,就去设置Entry,或者创建ThreadLocalMap
}
remove:
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* [email protected] #get read} by the current thread, its value will be
* reinitialized by invoking its [email protected] #initialValue} method,
* unless its value is [email protected] #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* [email protected] initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //计算key应在数组中的位置
//线性探测再散列,直到找到相同的key值(覆盖),或对应位置key为null(清理并设置)
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();
}
注释里解释流程比较方便。
为什么key要使用弱引用
使用弱引用是有一个GC的考虑。首先解释下什么是弱引用:
- 当对某个对象的引用只有弱引用时,一旦JVM进行GC时就会收回此对象。
为什么不使用强引用呢?需要结合上面的引用结构图来说明。
当线程不再使用某个ThreadLocal(假设叫tla),即不持有对tla的强引用的时候,GC就会自动回收tla,因为此时tla剩余的引用是Entry的key持有的弱引用
。此时,如果key是强引用就不会回收,造成了内存浪费。
但是由于value值是强引用,万一value值也不需要了,但是JVM又没有回收,就会造成内存泄漏。这里JDK的做法是:当系统进行ThreadLocalMap清理时,会自动将key值为null的 “垃圾数据” 回收。
当对ThreadLocal对象使用set、get、remove方法时,都会进行清理。
因此,移除ThreadLocal,不建议采用 tla = null
这样的做法,正确的做法是tla.remove()
,这样可以最大程度保证不会发生内存泄漏。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19353.html