ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
一个简单示例:
public class ThreadLocalTest01 implements Runnable{
//多线程共享数据
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public void run() {
setContent(Thread.currentThread().getName() + "的数据");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->" + getContent());
}
public static void main(String[] args) {
ThreadLocalTest01 test = new ThreadLocalTest01();
for(int index = 0; index < 5; index++) {
new Thread(test,"线程" + index).start();
}
}
}
可以看到结果是完全混乱的:
解决上述问题可以通过同步机制,也可以用ThreadLocal。
使用同步机制synchronized解决如下:
public void run() {
synchronized (ThreadLocalTest01.class) {
setContent(Thread.currentThread().getName() + "的数据");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->" + getContent());
}
}
加锁之后确实保证了线程安全,但是整个运行时长足足5s。这让程序失去了并发性。
使用ThreadLocal解决:
public class ThreadLocalTest01 implements Runnable{
private ThreadLocal threadLocal = new ThreadLocal();
public String getContent() {
return threadLocal.get();
}
public void setContent(String content) {
threadLocal.set(content);
}
public static void main(String[] args) {
ThreadLocalTest01 test = new ThreadLocalTest01();
for(int index = 0; index < 5; index++) {
new Thread(test,"线程" + index).start();
}
}
@Override
public void run() {
setContent(Thread.currentThread().getName() + "的数据");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->" + getContent());
}
}
上述两种方法都可以用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
原理 | 采用题时间换空间'的方式,只提供了一份变量,让不同的线程排队访问。 | 采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰。 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
其中set方法分析:
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程中维护的ThreadLocalMap对象,这个ThreadLocalMap是Thread类里面的成员
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != null)
//存在则存储数据(键:threadLocal对象 值:value)
map.set(this, value);
else
//不存在则进行ThreadLocalMap对象的初始化,并将t(当前线程)和value(t对应的值)存放至ThreadLocalMap中
createMap(t, value);
}
上述执行流程可概括为以下几点:
其中get方法分析:
public T get() {
Thread t = Thread.currentThread();
//获取此线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果此map存在则获取对应的存储实体
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//如果该实体e不为空,则返回我们之前存储的值
T result = (T)e.value;
return result;
}
}
//若map不存在执行当前代码
//若map存在没有与当前ThreadLocal关联的entry实体则执行当前代码
return setInitialValue();
}
private T setInitialValue() {
//获取初始化的值,该方法可以被子类重写,如果不重写默认返回null
T value = initialValue();
//获取当前线程的map
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//如果想重写此方法可以继承ThreadLocal类重写
protected T initialValue() {
return null;
}
上述执行流程可概括为以下几点:
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建井返回初始值。
其中remove方法分析:
//获取当前线程维护的map,若不为null则删除ThreadLocal对象对应的entry
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
所以通过源码可以看清楚,所以说为什么线程1里面set的数据线程2拿不到,就是因为set是往线程1里面的map中存的,跟线程2没有关系,线程2在获取的时候也是从自己的map中拿数据。
从上面代码可以看出,不论是get方法还是set方法都要先获取线程中维护的Map,如果map==null还需要进行createMap的操作。
createMap的流程和ThreadLocalMap部分源码如下:
void createMap(Thread t, T firstValue) {
//创建ThreadlocalMap对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//这是ThreadLocal类的静态内部类
static class ThreadLocalMap {
//map的初始容量(数组table的初始容量)
private static final int INITIAL_CAPACITY = 16;
//真正存放数据的数组
private Entry[] table;
//数组里面有效数据的个数
private int size = 0;
//进行扩容的阈值。表使用量大于它的时候扩容
private int threshold;
//Entry继承WeakReference,下面构造方法中调用父类单参构造方法,key(ThreadLocal对象)是弱引用。其目的是为了将ThreadLocal对象的生命周期和线程生命周期解绑。
static class Entry extends WeakReference> {
Object value;
//Entry的构造方法的k只能是ThreadLocal对象
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//数组的初始容量INITIAL_CAPACITY为16
table = new Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//存储数据
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
}
其实可以看到真正存储数据是通过一个Entry[] table来存储的,而且Entry是WeakReference(弱引用)的一个子类。其构造方法中显式调用父类单参构造方法,是让ThreadLocal对象的引用k变成弱引用指向ThreadLocal对象实例。在创建新ThreadLocalMap实例的时候,传入的firstKey正是最开始示例程序中我自己定义的属性成员threadLocal。然后又以这个threadLocal来实例化Entry对象。
那么让key(threadLocal)成为弱引用(WeakReference)有什么用。先来看一下几个概念:
Java中的引用有4种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用。
强引用(StrongReference) : 就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference) :垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
Memory overflow(内存溢出):没有足够的内存提供给申请者使用。
Memory leak(内存泄漏):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
要看清楚key弱引用的好处,来先看下假如它是强引用的情况下会造成什么后果:图形最清楚(图是看黑马视频时截的)
就以实例程序来分析:
ThreadLocalRef即为示例程序中的threadLocal。它指向ThreadLocal实例(那块堆空间)。再透过源码可以看到Entry对象的Key也是指向ThreadLocal的那份实例空间的。如果我来个threadLocal==null(即图中擦去ThreadLocalRef的指向);若只有threadLocal指向那块空间,则本来应该回收空间。但是现在的局面,就算threadLocal==null了,Key还引用着那块空间且是强引用(此处讨论强引用情况)。只要Entry空间不被回收,那么Key就一直引用着ThreadLocal的内存空间,通过指向可以看出,只有当当前线程结束的时候ThreadLocal内存空间才会被回收。这就存在着内存泄漏的问题。
现在看Key是弱引用的情况:
如果是弱引用,那么在ThreadLocalRef引用结束之后,只要触发gc,ThreadLocal的内存空间就会被回收。 像图中分析的一样,即使是弱引用也会存在内存泄露问题。解决了ThreadLocal的内存泄漏,此时Key==null了,但是value(引用类型,占堆空间)的内存空间再也不会被访问到了,但是没有释放。造成这些“脏”entry存在。
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove ,无论key是强引用还是弱引用都不会有问题。
那么为什么Key要使用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除 ,从而避免内存泄漏。
首先从上面ThreadLocalMap的源码中可以看到其构造方法逻辑:
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//数组的初始容量INITIAL_CAPACITY为16
table = new Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//存储数据
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
其中int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);firstKey就是ThreadLocal对象。
对于threadLocalHashCode将其中涉及到的代码摘抄出来如下:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
/*
此处HASH_INCREMENT 是为了让哈希码能均匀的分布在2的N次方的数组里(也就是Entry[] table)。
保证了散列表的离散度,从而降低了冲突的几率。所以数组table的大小必须是2的N次方
*/
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//一个特殊的hash值,这个值跟斐波那契数列有关系。
private static final int HASH_INCREMENT = 0x61c88647;
//AtomicInteger 是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发的情况下使用。
private static AtomicInteger nextHashCode = new AtomicInteger();
hashCode & (size-1);这相当于hashCode % size。这也能保证在索引不越界的前提下,降低发生hash冲突的几率。
再看一下ThreadLocalMap的set方法:
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//使用线性探测法查找元素
//根据计算出的索引,从数组中取出一个元素e,对其进行判空。
for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
//如果不为空则取出对应的键(ThreadLocal对象)
ThreadLocal> k = e.get();
//如果该键已经存在,则覆盖其值。
if (k == key) {
e.value = value;
return;
}
/*
如果该键不存在(e不为null,但k为null。
说明存在之前的ThreadLocal对象的强引用已经被释放掉了)当前数组中的Entry是一个陈旧的元素。
用新元素替换旧元素。
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//创建一个新的Entry实例,插入在空元素位置处
tab[i] = new Entry(key, value);
int sz = ++size;
/*
清除那些key已经为null,但值还存在的entry(这个可以避免内存泄漏)。
如果没有清除任何entry,且当前的使用量已经达到了阈值,则进行一次全表的扫描清理。
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//当i >= len-1(即数组最后一个索引时),返回的是0;否则让数组索引每次增加1。这相当于一个循环数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
上述代码流程可大致总结为如下几点: