一、问题引入
多线程访问同一个共享变量时特别容易出现并发问题,尤其是多个线程需要对一个共享变量进行写入时。
可以通过以下方法保证线程安全:
- 加锁
- 使用线程本地变量,也就是ThreadLocal
二、ThreadLocal原理
- Thread类中有一个threadLocals和一个inheritableThreadLocals,都是ThreadLocalMap类型的变量
- ThreadMap是一个定制化的Hashmap
- 当前线程第一次调用ThreadLocal的set/get方法才会创建这两个变量
- ThreadLocal类型的本地变量存放在具体的线程内存空间中,ThreadLocal是一个工具壳,通过set方法把value值放入调用线程的treadLocals;通过get方法从当前线程的threadLocals变量将其拿出来
- 可以调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量
Question:Thread里面的threadLocals为什么要设计为Map结构?
因为每个线程可以关联多个ThreadLocals变量。
- 源码分析
set()方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 把当前线程作为key,查找线程本地变量对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// map不为空则直接设置值
if (map != null)
map.set(this, value);
// 第一次调用则创建线程对应的ThreadLocalMap
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
// 如果threadLocals不为null,返回对应本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// treadLocals为null则初始化当前线程的threadLocals成员变量
return setInitialValue();
private T setInitialValue() {
// 初始化为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果当前线程的threadLocals变量不为空
if (map != null)
map.set(this, value);
// 如果当前线程的threadLocals变量为空
else
createMap(t, value);
return value;
}
remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
Question:为什么在不使用本地变量之后需要对其remove掉?
- 每个线程内部都有一个threadLocals的成员变量,该变量的类型是HashMap,其中key被定义的ThreadLocal变量的this引用,value是set方法设置的值。
- 每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,这些本地变量会一直存在,可能会造成内存溢出。因此使用完毕之后需要调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。
PS:对应线程的声明周期,一个线程运行结束就是销毁状态。
三、代码实现
public class TestThreadLocal {
// print函数
static void print(String str){
// 打印当前线程本地内存中变量的值
System.out.println(str+":"+localVariable.get());
// 清除当前线程本地内存中的localVariable变量
// localVariable.remove();
}
// 创建ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
// 创建线程Thread1
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// 设置thread1中本地变量localVariable的值
localVariable.set("thread1 local Variable");
// 调用答应函数
print("thread1");
// 打印本地变量值
System.out.println("thread1 remove after:"+localVariable.get());
// 睡眠1s后再次打印看变量是否被覆盖
try {
Thread.sleep(1000);
System.out.println("thread1 remove after:"+localVariable.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
// 设置thread1中本地变量localVariable的值
localVariable.set("thread2 local Variable");
// 调用打印函数
print("thread2");
// 打印本地变量值
System.out.println("thread2 remove after:"+localVariable.get());
// 睡眠1s后再次打印看变量是否被覆盖
try {
Thread.sleep(1000);
System.out.println("thread1 remove after:"+localVariable.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
可以看到,线程1和线程2各自对线程本地内存的变量进行操作,互不影响,是线程安全的。
把删除本地变量的注释取消掉并取消休眠:
四、一个应用
在SpringBoot中注入HttpSertletRequest获取会话信息,而HttpServletRequest是一个ThreadLocal变量,所以在多线程环境下,各个线程操作的都是自己本地的副本,是线程安全的。