多线程并发操作一个共享变量时会造成线程安全问题,使用ThreadLocal可以保证共享变量线程安全。由于同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本,当线程访问ThreadLocal时,每个线程都操作自己的独立副本,避免造成线程安全问题。
简单使用
每个线程分别对ThreadLocal赋值、打印。
static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
new Thread("thread-1") {
@Override
public void run() {
operateThreadLocal("thread-1");
}
}.start();
new Thread("thread-2") {
@Override
public void run() {
operateThreadLocal("thread-2");
}
}.start();
}
/**
* 操作ThreadLocal
*
* @param value
*/
public static void operateThreadLocal(String value) {
threadLocal.set(value);
System.out.println(threadLocal.get());
threadLocal.remove(); // 使用完对其进行remove,防止内存泄露
}
源码分析
ThreadLocal源码比较简单,内部基于ThreadLocalMap对每个线程设置的值进行保存,其中key为线程,value为设置的值。获取时根据当前线程获取。
set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
// 为null创建map
else
createMap(t, value);
}
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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key值相等进行替换
if (k == key) {
e.value = value;
return;
}
// 该位置原key为null,则替换为新key,value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 创建entry放入数组中,key为WeakReference弱引用,下次gc会被回收
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map不为null则查找set放入的entry
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
remove方法:由于set时entry的key为弱引用,发生gc会被回收,根据可达性分析法,其value永远不会被回收,无法释放占用的内存空间造成内存泄露。所以用完之后通过remove方法清理entry可防止内存泄露。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清除对象引用
e.clear();
// 清除value,修改数组大小
expungeStaleEntry(i);
return;
}
}
}