目录
1 ThreadLocal概述
我们知道单例模式有一个明显的缺点,就是存在并发问题。多线程修改、访问同一个对象的时候会出现与预期不相符的结果。解决并发问题可以通过加锁,但是存在性能问题。
另一个方式就是可以用ThreadLocal,把对象做成一个线程级别的变量,每个线程都维护一个数据的副本,实现线程级别的数据隔离,也是典型的拿空间换时间。
2 ThreadLocal使用
@Test
public void testThreadLocal() throws InterruptedException {
new Thread(() -> {
setData(1);
getData();
}, "线程一").start();
new Thread(() -> {
setData(2);
getData();
}, "线程二").start();
TimeUnit.MINUTES.sleep(1);
}
private void setData(Integer d) {
data.set(d);
System.out.println(Thread.currentThread().getName() + " set data:" + data.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void getData() {
System.out.println(Thread.currentThread().getName() + " get data:" + data.get());
}
输出:
线程一 set data:1
线程二 set data:2
线程一 get data:1
线程二 get data:2
可以看到每个线程设置、访问数据是不收影响的
3 ThreadLocal原理
3.1 set方法
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果map不为空则存入数据,key-ThreadLocal对象,v-存放的数据
map.set(this, value);
else
// 如果map为空则创建一个map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
阅读源码可知每个线程维护了一个map,这个map用来存放ThreadLocal和数据的映射关系。存放数据的时候首先获取当前线程的map,然后在map中存放数据
3.2 get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取该ThradLocal对象绑定的entry,进而获得数据
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get方法很简单,就是先根据当前线程获取map,然后根据map读取的对象
3.3 remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove方法就是把当前线程绑定的数据移除,比较简单,这里不做太多分析
4 使用ThreadLocal注意事项
一般我们项目中都会使用到线程池,线程池中的线程是可以复用的,所以每次使用完ThreadLocal的时候要清理一下数据,否则用线程池执行下一个任务的时候可能会获取到旧数据
测试代码:
private ThreadLocal<Integer> data = new ThreadLocal<>();
private Executor executor = Executors.newFixedThreadPool(1);
@Test
public void testThreadLocal() throws InterruptedException {
executor.execute(() -> {
setData(1);
getData();
});
executor.execute(() -> getData());
TimeUnit.MINUTES.sleep(1);
}
private void setData(Integer d) {
data.set(d);
System.out.println(Thread.currentThread().getName() + " set data:" + data.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void getData() {
System.out.println(Thread.currentThread().getName() + " get data:" + data.get());
}
输出:
pool-1-thread-1 set data:1
pool-1-thread-1 get data:1
pool-1-thread-1 get data:1
可以看到线程池执行第二个任务的时候仍然获取到第一个任务中存放的数据
get数据的正确姿势(当然移除的时候确定后续过程不再需要访问data了)
private void getData() {
Integer d = data.get();
data.remove();
System.out.println(Thread.currentThread().getName() + " get data:" + d);
}
再测试:
pool-1-thread-1 set data:1
pool-1-thread-1 get data:1
pool-1-thread-1 get data:null