java ThreadLocal 实践与面试要点
Posted BBinChina
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java ThreadLocal 实践与面试要点相关的知识,希望对你有一定的参考价值。
问题:
1、线程共享进程的内存,如果我们需要线程间各自维护数据避免数据污染。
2、线程变量跨线程池传递
解决方案
定义一个map,key为线程id,value为我们要隔离的变量。
map<threadId, value> threadMap
常见使用场景
通常在系统开发时,初期为了快速开发或者逻辑设计没有很清晰的情况下,一个函数可能包含了多个处理逻辑,为了系统代码结构整洁,符合开闭单一原则,我们需要对函数进行分解,这个时候会发现
多个子函数会用到同一份数据, 比如当一个请求处理时, A函数会远程调用其他服务获取数据,这份数据同样在B函数会用到,那么在拆解的过程中,可以通过参数传递的方式将数据传递给每个需要的子函数。完成第一步重构后,会发现可能传递的参数太多了怎么办,而实际上在一次请求时,这些数据的传递只不过是在一个线程内的状态传递,而这种需要传递的对象,我们通常称之为上下文(Contetxt),这个时候便可以使用 Threalocal来传递数据。
实践
Java标准库提供ThreadLocal类,它可以在一个线程中传递同一个对象。
ThreadLocal实例通常总是以静态字段初始化如下:
static ThreadLocal<Data> threadLocalData = new ThreadLocal<>();
它的典型使用方式如下:
void request(data) {
try {
threadLocalData.set(data);
A();
B();
} finally {
threadLocalData.remove();
}
}
同理我们提出的解决方案,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。
最后,特别注意ThreadLocal一定要在finally中清除:
try {
threadLocalData.set(data);
...
} finally {
threadLocalData.remove();
}
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
虽然ThreadLocalMap使用了弱引用key,而弱引用的释放发生在垃圾回收,由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏。
扩展
ThreadLocal为什么会内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
ThreadLocal不是线程安全
ThreadLocal设置线程变量的源码
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
其中的getMap方法
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这个方法在InheritableThreadLocal被重写,所以InheritableThreadLocal可以用来解决不安全的问题,回过头来看其不安全的原因。共用的ThreadLocal虽然按线程存储了ThreadLocalMap用来存放线程的变量,但存放的内容Value是一个引用值,当有其他线程对其进行修改时,当前线程读取的数据是已改后的值。例子如下:
class UnSafeThread implements Runnable {
public static final ThreadLocal<Number> value = new ThreadLocal<Number>();
public static Number number = new Number();
public static int i = 0;
@Override
public void run() {
number.setNum(i++);
value.set(number);
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread()+" 输出 "+number.getNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上文提到的InheritableThreadLocal 可以解决ThreadLocal的局限性,当同时也有其局限
public class TestThreadLocal {
public static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
public static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(1);
@Test
public void fun1() {
THREAD_LOCAL.set(new Person());
THREAD_POOL.execute(()->{getAndPrintData();});
Person person = new Person();
person.setAge(100);
THREAD_LOCAL.set(person);
THREAD_POOL.execute(()->{getAndPrintData();});
}
private void setData(Person person) {
System.out.println("set 数据,线程名:"+Thread.currentThread().getName());
THREAD_LOCAL.set(person);
}
private Person getAndPrintData() {
Person person = THREAD_LOCAL.get();
System.out.println("get 数据,线程名:"+Thread.currentThread().getName()+" 数据为: "+person.toString());
return person;
}
}
当线程池数目为1时,两次执行会复用同一个线程,输出内容如下:
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
会发现Person的age同为18,这是因为复用线程内的ThreadLocalMap为同一个,所以不会绑定新的数据
当线程池数目为2时,即使线程池有空闲线程,而为达到CoreSize时,依然会初始化新线程,两次执行结果如下:
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-2,数据为:TestThreadLocal.Person(age=100)
线程的Init初始化过程会重新绑定值,所以第二线程输出的age为100.
InheritableThreadLocal 在Init时的源码
在Thread Init函数中有核心的处理部分,InheritableThreadLocals会将父类的ThreadLocalMap传递给子类
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
虽然实现了父子线程的变量传递,但仍局限于非父子线程间的变量传递,比方说 调用链跟踪的使用场景(也有其他方式解决)。
针对跨线程池,阿里提出了TTL的组件。
TransmittableThreadLocal
TTL是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。
TransmittableThreadLocal简称TTL,InheritableThreadLocal简称ITL
它的官网是:TTL
以上是关于java ThreadLocal 实践与面试要点的主要内容,如果未能解决你的问题,请参考以下文章