文章目录
- 一、为什么需要ThreadLocal
- 二、ThreadLocal介绍
- 三、ThreadLocal源码分析
- 1、先来看看get()方法
- 2、再看set()方法
- 3、总结
一、为什么需要ThreadLocal
public class ThreadLocalTest {
private static Integer num = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
num += 5;
System.out.println(Thread.currentThread().getName() + "->" + num);
}, "Thread-" + i);
threads[i].start();
}
}
}
我们可以看到每个线程拿到的值都是不一样的,即它们共享了变量资源。
思考以下几种情况:
每个用户请求过来之后,它的数据是受保护的,隔离的。比如第一个用户的id是1,第二个用户的id是2。那么它把id改成了2,第一个用户拿到的id就是2了(我们希望拿到的是1),这样线程之间就会相互影响,是不允许存在的。
那我们想要每个线程都拿到各自自己的值该怎么做呢?
ThreadLocal可以解决这个问题
/**
* ThreadLocal:每个线程有自身的存储本地、局部区域
* 方法:get/set/initialValue
*/
public class ThreadLocalTest {
private static Integer num = 0;
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;//初始值
}
};
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
int num = local.get();
num += 5;
local.set(num);
System.out.println(Thread.currentThread().getName() + "->" + num);
}, "Thread-" + i);
threads[i].start();
}
}
}
我们可以看到每个线程拿到的值都是一样的,线程之间没有相互影响。
二、ThreadLocal介绍
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。
ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。可以使用 get 和 set 方法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
再举个简单的例子: 比如有两个⼈去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配⼀个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
三、ThreadLocal源码分析
1、先来看看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);//获取t线程的threadLocals
if (map != null) {//map不为空
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//map为空的话,就赋初试值
return setInitialValue();
}
再看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;
}
Thread类里面有个属性是threadLocals ,其类型是ThreadLocalMap(ThreadLocal的静态内部类),就相当于每个线程都有一个集合去存储数据,那是不是就可以实现隔离了。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
get()方法里的map为空时,就要赋一个初始值:
private T setInitialValue() {
T value = initialValue();//value为初始值
Thread t = Thread.currentThread();//得到当前线程t
ThreadLocalMap map = getMap(t);//获取t线程的threadLocals
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
我们可以重写initialValue()方法,那么其初始值就不是null了。
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;//初始值
}
};
setInitialValue(),如果map为空,就调用createMap()方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);//实例化一个ThreadLocalMap对象
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//INITIAL_CAPACITY大小为16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//计算hash
table[i] = new Entry(firstKey, firstValue);//把key和value放进去
size = 1;
setThreshold(INITIAL_CAPACITY);
}
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[]数组来存储<key,value>键值对,key是ThreadLocal的一个对象(也就是当前线程),value是初始值initialValue。
2、再看set()方法
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//获取t线程的threadLocals
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
看ThreadLocalMap 里的 map.set()方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);//计算hash,用i找到对应的Entry数组元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//循环找
ThreadLocal<?> k = e.get();//获取k
if (k == key) {//找到了key,就覆盖value
e.value = value;
return;
}
//key 为 ThreadLocal 的弱引⽤,key为空就需要回收掉,不然会造成内存泄露
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//数据超过16,就需要扩容
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3、总结
从Thread 类的源代码可以看出 Thread 类中有⼀个 threadLocals 和 ⼀个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为 ThreadLocal 类实现的定制化的 HashMap。
默认情况下这两个变量都是 null,只有当前线程调⽤ ThreadLocal 类的 set 或 get ⽅法时才创建它们,实际上调⽤这两个⽅法的时候,我们调⽤的是 ThreadLocalMap 类对应的 get() 、 set() ⽅法。
我们可以得出结论:
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上, ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal类中可以通过 Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象。
我们在同⼀个线程中声明了两个 ThreadLocal 对象的话,都是使用那个 ThreadLocalMap 存放数据的,ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是 ThreadLocal 对象调⽤ set 方法设置的值。
再了解下ThreadLocal的内存泄露问题:
ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,而value 是强引⽤。
所以,如果 ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,而value 不会被清理掉。
这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set() 、 get() 、 remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用 remove() 方法。