文章目录
深入浅出ThreadLocal
1.特性
多线程环境下,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
可以简单理解为不同线程之间,变量不会互相影响
2.测试例子
2.1代码1
public class ThreadLocalTest {
private static int num = 0;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
num += 5;
System.out.println(Thread.currentThread().getName() + "---->" + num);
}, "Thread" + i).start();
}
}
}
执行结果:
大部分情况下线程拿到的num都是其他线程累加之后的值,当然在并发情况下会出现线程安全问题(这里不做讨论)
怎么让每个线程都操作的是自己的num呢?
2.2代码2
public class ThreadLocalTest1 {
private static ThreadLocal<Integer> local = new ThreadLocal() {
@Override
protected Object initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int num = local.get() + 5;
System.out.println(Thread.currentThread().getName() + "---->" + num);
}, "Thread" + i).start();
}
}
}
执行结果:
线程从local中取到的值都是0,线程间并没有影响
图解:
每个线程访问ThreadLocal对象时,ThreadLocal都会返回一个Integer为0的副本
2.3代码3
这里还有一个基本类型
和引用类型
的区别
详情可以看这篇文章 java是值传递还是引用传递?
代码2中的例子为基本类型的包装类型,本例子中则是用Index对象引用类型;
ThreadLocal初始化返回的是Index的引用类型
public class ThreadLocalTest2 {
private static Index index = new Index();
private static ThreadLocal<Index> local = new ThreadLocal() {
@Override
protected Object initialValue() {
return index;
}
};
static class Index {
Integer num = 0;
public void inrc() {
num++;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Index index = local.get();
index.inrc();
System.out.println(Thread.currentThread().getName()+ "---->" + local.get().num);
},"Thread"+i).start();
}
}
}
执行结果:
明明用了ThreadLocal,但是值还是互相影响了
原因是Index对象是引用类型,操作的是引用地址的副本,但是引用地址指向的还是同一个堆中的内存地址,所以最终修改同一个内存地址中的值,还是影响到了其它的线程中的num变量
将
private static ThreadLocal<Index> local = new ThreadLocal() {
@Override
protected Object initialValue() {
return index;
}
};
修改为
private static ThreadLocal<Index> local = new ThreadLocal() {
@Override
protected Object initialValue() {
return new Index();
}
};
就可以了,每次返回一个新的对象(一个新的内存地址,就不会影响其它线程)
有图有真相,执行效果
3.源码分析
3.1类分析
环境:jdk1.8
ThreadLocal实现线程变量隔离的数据结构
Thread类
每个线程都有一个ThreadLocakMap的对象
ThreadLocal类
ThreadLocal的ThreadLocalMap对象内部存放的是一个Entry对象
部分代码:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
......
3.2ThreadLocal类中的get()方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
-
获取当前线程
-
获取线程的ThreadLocalMap对象
此方法直接返回的是当前线程的threadLocals属性
3.如果不为null,则返回ThreadLocalMap存放的值
4.如果为null,初始化
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocalMap map = getMap(t);
获取当前线程的threadLocals值
- 如果不为null就设置key为this当前ThreadLocal实例对象,value为这个值
- 如果为null则调用createMap方法继续初始化,key为当前线程,value为
initialValue()
方法的值,此方法为我们重载写的方法。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这一步就将当前线程的threadLocals属性设置为一个ThreadLocalMap的对象。
3.3ThreadLocal类中的set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
过程和get()方法基本一致
3.4结论
经过上面的过程,保证每个线程都有一个ThreadLocalMap的对象,这个对象的key为ThreadLocal的实例对象,值就是我们set()
设置的值或者是重写initialValue()
方法的返回值;通过这样的方式实现了线程变量的隔离效果。
4.开源框架中的应用
4.1shiro 存储当前用户subject的信息
shiro中使用aop切面的方式处理请求,进行权限的验证,获取用户权限时,aop切面中很难通过传入的参数进行获取,而ThreadLocal可以很好的突破这种限制
在发起请求的时候,线程和subject(当前用户)做绑定,进入aop切面的时候,从threadContext中取出subject(当前用户)
源码:
权限验证方法
获取Subject方法
从ThreadContext类中获取
ThreadContext 部分代码
private static final ThreadLocal<Map<Object, Object>> resources
核心代码
public abstract class ThreadContext {
......
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
public static Subject getSubject() {
return (Subject) get(SUBJECT_KEY);
}
......
4.2mybatis 日志打印处理
在一个每个操作数据库请求,每一个关键步骤都使用ErrorContext进行了日志的收集,内部就是用ThreadLocal实现。
当一个线程报错的时候,通过追踪日志打印的信息可以判断在哪一步错误,不同的线程日志信息不会互相影响
部分代码
核心代码private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
public class ErrorContext {
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
public ErrorContext store() {
ErrorContext newContext = new ErrorContext();
newContext.stored = this;
LOCAL.set(newContext);
return LOCAL.get();
}
......