多线程线程安全的根源就是“共享”,即多个线程操作共享变量会引起可见性、原子性和顺序性的问题。解决线程安全首先我们想到的是加锁,比如我们熟知的Synchronized和ReentrantLock等,这里介绍另外一种解决共享问题的模式,线程本地储存模式,没有共享就没有伤害,每个线程都有自己的变量,彼此之间不共享,Java中实现这一思想的就是ThreadLocal类。
如果我们自己去实现一个ThreadLocal,相信大家都会想到用HashMap这种数据结构能够完成,很多了解ThreadLocal的人也说ThreadLocal其实就是一个HashMap,其实这种说法不是很准确,我们来看看JDK源码是如何实现ThreadLocal的,首先看一下ThreadLocal类的结构,ThreadLocalMap和SuppliedThreadLocal是两个内部类,ThreadLocalMap就是用来储存线程变量的,和HashMap类似其内部维护一个Entry数组,初始大小都是16,但是ThreadLocalMap不存在负载因子的说法,因为它的key计算如下所示,
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
是原子加一操作,不会存在hash冲突问题,当Entry数组满了之后会按照乘以2的方式扩容。SuppliedThreadLocal应该是JDK1.8引入的,采用函数式的编程思想通过Supplier.get()给ThreadLocal赋值。下面着重分析一下ThreadLocal的几个重要方法:
1)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;
}
}
//此处value为null
return setInitialValue();
}
private T setInitialValue() {
//return null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
get方法首先会获取当前线程,这里可以看到ThreadLocalMap不是ThreadLocal类持有的,而是Thread类持有的,这里不得不佩服JDK大佬们对现象对象程序设计理解之深入骨髓,线程局部变量是属于线程的,应该封装在Thread类中。获取map之后通过hash获取value,如果不存在就初始化ThreadLocalMap的value为空,并返回null。
2)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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
其实也比较简单,首先获取当前线程的ThreadLocalMap对象,如果不为空则塞值,如果为空则根据value初始化ThreadLocalMap。
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("子线程Id为:" + printThreadId());
});
thread.start();
thread.join();
System.out.println("主线程Id为:" + printThreadId());
}
static int printThreadId() {
return ThreadId.get();
}
}
class ThreadId {
private static final AtomicInteger nextId = new AtomicInteger(0);
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());
public static int get() {
return threadId.get();
}
}
//运行结果为
子线程Id为:0
主线程Id为:1
上面展示了ThreadLocal的基本用法,实现每个线程分配一个线程ID。
1)内存泄漏
这个我并未遇到过,但是这好像是一个很重要的知识点(手动滑稽),内存泄漏的原因是,ThreadLocalMap是Thread维护的(前面还说JDK大佬的思想无敌,现在又说给我们使用留下的坑,再次手动滑稽,当然多半是我们使用不当),生命周期和Thread一样,ThreadLocalMap中的value无法被回收,因此应该手动remove。
2)异步化带来的坑
JDK1.8的CompletableFuture的引入使得异步编程更加简单,代码中各种开异步线程,有时候我们为了避免方法参数的链式传递,将基本的方法参数放在ThreadLocal中进行传递,比如一个办公系统中员工的工号和认证token,如果使用ThreadLocal,异步线程中就会出问题了,这时可以使用InheritableThreadLocal,支持子线程继承父线程的线程局部变量。当然我还遇到过更加奇葩的问题,通过消息中间件MQ,生产者和消费者部署在不同的服务上,在生产者端将基本参数放入ThreadLocal中,然后在消费端尝试获取这些参数导致出现问题。
网友评论