ThreadLocal(基于JDK1.8)

知识点1:ThreadLocal是什么?

需要理解线程安全,简单来说造成线程不安全的原因有两个。

  • 不想共享的变量被共享了

  • 想共享的没及时共享

ThreadLocal解决的是第一个问题,很多情况下我们不希望不同线程之间能互相访问操作对方的变量。例如一个web服务器,多个用户并发访问,用户A有用户A的userId,用户B有用户B的userId。这时候可以使用ThreadLocal保存每个访问线程对应的userId,各读各的,互不干扰。

权威解释

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its{@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g. a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the {@code ThreadLocal} instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

不翻译了,记几点解释说明如下:

  • ThreadLocal对象通常是private static fields

  • 线程运行结束后可以对该线程的变量副本进行回收,除非该副本有别的引用

  • 关键字:副本,线程独享

一个例子,运行两个线程分别设置userId的值,可以看到不同线程互不干扰。

 
  1. public class Test {

  2.  

  3.    private static ThreadLocal<String> userId = ThreadLocal.withInitial(() -> "init_id");

  4.  

  5.    public static void main(String[] args) throws InterruptedException {

  6.        Thread thread1 = new Thread(() -> {

  7.            try {

  8.            // 线程1两秒之后获得userid,并且设置userid为id1

  9.                TimeUnit.SECONDS.sleep(2);

  10.                System.out.println("initial userId in thread1:" + userId.get());

  11.                userId.set("id1");

  12.                System.out.println("thread1 set userId id1");

  13.            } catch (InterruptedException e) {

  14.                e.printStackTrace();

  15.            }

  16.        });

  17.  

  18.        Thread thread2 = new Thread(() -> {

  19.            try {

  20.                // 线程二获取初始的userId,然后一秒之后设置为id2,再过两秒之后再次读取userid

  21.                System.out.println("initial userId in thread2:" + userId.get());

  22.                TimeUnit.SECONDS.sleep(1);

  23.                userId.set("id2");

  24.                System.out.println("thread2 set userId id2");

  25.                TimeUnit.SECONDS.sleep(2);

  26.                System.out.println("now userId in thread2:" + userId.get());

  27.  

  28.            } catch (InterruptedException e) {

  29.                e.printStackTrace();

  30.            }

  31.        });

  32.  

  33.        thread1.start();

  34.        thread2.start();

  35.  

  36.        // 在main线程等待两个线程执行结束

  37.        thread1.join();

  38.        thread2.join();

  39.  

  40.    }

  41. }

 

 
  1. initial userId in thread2:init_id

  2. thread2 set userId id2

  3. initial userId in thread1:init_id

  4. thread1 set userId id1

  5. now userId in thread2:id2

知识点二:ThreadLocal实现原理

最关键代码如下,在任何代码中执行Thread.currentThread(),都可以获取到当前执行这段代码的Thread对象。既然获得了当前的Thread对象了,如果让我们自己实现线程独享变量怎么实现呢?自然而然就会想到在先获取当前Thread对象,然后在当前Thread对象中使用一个容器来存储这些变量,这样每个线程都持有一个本地变量容器,从而做到互相不干扰。

public static native Thread currentThread();

而jdk确实是这么实现的,Thread类中有一个ThreadLocalMap threadLocals。ThreadLocalMap是一个类似HashMap的存储结构,那么它的key和value分别是什么呢?

 
  1. public class Thread implements Runnable {

  2. ...

  3.    /* ThreadLocal values pertaining to this thread. This map is maintained

  4.     * by the ThreadLocal class. */

  5.    ThreadLocal.ThreadLocalMap threadLocals = null;

  6. ...

  7. }

如下,key是一个ThreadLocal对象,value是我们要存储的变量副本

 
  1.  static class Entry extends WeakReference<ThreadLocal<?>> {

  2.     /** The value associated with this ThreadLocal. */

  3.     Object value;

  4.  

  5.     Entry(ThreadLocal<?> k, Object v) {

  6.        super(k);

  7.        value = v;

  8.     }

  9.  }

至此,一切都明了了。

  • ThreadLocal对象通过Thread.currentThread()获取当前Thread对象

  • 当前Thread获取对象内部持有的ThreadLocalMap容器

  • 从ThreadLocalMap容器中用ThreadLocal对象作为key,操作当前Thread中的变量副本。

调用链如下:

例子中的存储结构简图如下:

关键代码在ThreadLocal类中:

 
  1.    public void set(T value) {

  2.        // 获取当前线程的Thread对象

  3.        Thread t = Thread.currentThread();

  4.        // 获取当前线程的ThreadLocalMap对象

  5.        ThreadLocalMap map = getMap(t);

  6.        // 如果map不为空,直接设置值

  7.        if (map != null)

  8.            map.set(this, value);

  9.        else

  10.            // 如果为空,先创建map再设置值

  11.            createMap(t, value);

  12.    }

  13.  

  14.  

  15.    public T get() {

  16.        Thread t = Thread.currentThread();

  17.        ThreadLocalMap map = getMap(t);

  18.        // 如果不为空返回map中的value

  19.        if (map != null) {

  20.            ThreadLocalMap.Entry e = map.getEntry(this);

  21.            if (e != null) {

  22.                @SuppressWarnings("unchecked")

  23.                T result = (T)e.value;

  24.                return result;

  25.            }

  26.        }

  27.        // 否则返回初始值

  28.        return setInitialValue();

  29.    }

知识点三:ThreadLocal的内存泄露问题

ThreadLocal为什么会有内存泄露问题?

因为程序员对ThreadLocal理解不足(或者说jdk过度封装使程序员对ThreadLocal理解不足)而造成的容器清理不及时。

ThreadLocal本质上只是对当前Thread对象中ThreadLocalMap对象操作的一层封装,我们始终操作的只是一个map而已。当这个map一直存活(线程一直存活),并且我们忘了清除这个map中我们已经不需要的entry,就会造成内存泄露。

一个例子

 
  1. public class Test3 {

  2.    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

  3.  

  4.        Object[] entrys = getEntrys();

  5.  

  6.        System.out.println("初始化threadLocalMap的entrys数量为:" + getSize(entrys));

  7.  

  8.        ThreadLocal<String> userId = ThreadLocal.withInitial(() -> "init id");

  9.        userId.set("id in main thread");

  10.  

  11.        System.out.println("设置userId后threadLocalMap的entrys数量为:" + getSize(entrys));

  12.  

  13.        // 失去Threadlocal对象的强引用,并且尝试调用gc回收

  14.        userId = null;

  15.        System.gc();

  16.  

  17.        System.out.println("userId置null后threadLocalMap的entrys数量为:" + getSize(entrys));

  18.  

  19.    }

  20.    // 获得数组中非null元素个数

  21.    private static int getSize(Object[] objects) {

  22.        int count = 0;

  23.        for (Object object : objects) {

  24.            if (object != null) {

  25.                count++;

  26.            }

  27.        }

  28.        return count;

  29.    }

  30.  

  31.    // 通过反射获得ThreadLocalMap中的底层数组

  32.    private static Object[] getEntrys() throws NoSuchFieldException, IllegalAccessException {

  33.        Thread mainThread = Thread.currentThread();

  34.        Field threadLocals = Thread.class.getDeclaredField("threadLocals");

  35.        threadLocals.setAccessible(true);

  36.        Object threadLocalMap = threadLocals.get(mainThread);

  37.        Class<?>[] declaredClasses = ThreadLocal.class.getDeclaredClasses();

  38.        Field table = declaredClasses[0].getDeclaredField("table");

  39.        table.setAccessible(true);

  40.        return (Object[]) table.get(threadLocalMap);

  41.    }

  42.  

  43. }

执行结果

 
  1. 初始化threadLocalMap的entrys数量为:2

  2. 设置userId后threadLocalMap的entrys数量为:3

  3. userId置null后threadLocalMap的entrys数量为:3

从上述结果可以看出,当ThreadLocal对象失去引用之后,ThreadLocalMap中相应的entry并未删除。从而产生内存泄露。如下图:

网上一些说因为弱引用造成内存泄露的说法是错误的

如上图所示,虚线代表弱引用,当没有强引用指向ThreadLocal对象时也会被回收,value回收不了。但是问题的根源不是弱引用,而是没有把entry从map中移除。ThreadLocalMap中key的弱引用代码如下,弱引用至少可以在你忘了移除ThreadLocalMap对应entry的时候帮你删除entry中的key,可以说这个弱引用有益无害。弱引用表示这个锅我不背。

 
  1.  static class Entry extends WeakReference<ThreadLocal<?>> {

  2.  /** The value associated with this ThreadLocal. */

  3.       Object value;

  4.  

  5.       Entry(ThreadLocal<?> k, Object v) {

  6.       // key也就是ThreadLocal对象在ThreadLocalMap的Entry中是一个弱引用

  7.          super(k);

  8.          value = v;

  9.       }

  10.  }

其实不仅仅是ThreadLocal,我们操作数组、集合、Map等任何容器。如果这个容器生命周期比较长,我们都应该注意remove掉不再需要的元素。而且Map中的key最好是不可变元素(ThreadLocal也最好为final的)。

怎么防范内存泄露

很简单,使用完毕之后,调用ThreadLocal对象的remove()方法,实际上也是对ThreadLocalMap删除entry的一层包装。

 
  1.     public void remove() {

  2.         ThreadLocalMap m = getMap(Thread.currentThread());

  3.         if (m != null)

  4.             m.remove(this);

  5.     }

知识点四:线程池与ThreadLocal

线程池既然是线程独享的,那么当使用线程池的时候,是怎么操作的呢?

很简单,每次使用完毕后remove.

下面是spring web MVC实现RequestContextHolder线程隔离的关键代码。

 
  1.    // org.springframework.web.context.request.RequestContextHolder

  2.    /**

  3.     * Reset the RequestAttributes for the current thread.

  4.     * 重置当前线程的request属性,就是调用ThreadLocal对象的remove()方法

  5.     */

  6.    public static void resetRequestAttributes() {

  7.        requestAttributesHolder.remove();

  8.        inheritableRequestAttributesHolder.remove();

  9.    }

  10. // requestAttributesHolder实际上就是ThreadLocal一个对象

  11. private static final ThreadLocal<RequestAttributes> requestAttributesHolder =

  12.    new NamedThreadLocal<>("Request attributes");

  13.    // org.springframework.web.context.request.RequestContextListener

  14.    // 重写ServletRequestListener的方法,request初始化之后调用

  15.    @Override  

  16.    public void requestInitialized(ServletRequestEvent requestEvent) {

  17.        if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {

  18.            throw new IllegalArgumentException(

  19.                    "Request is not an HttpServletRequest: " + requestEvent.getServletRequest());

  20.        }

  21.        HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();

  22.        ServletRequestAttributes attributes = new ServletRequestAttributes(request);

  23.        request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);

  24.        LocaleContextHolder.setLocale(request.getLocale());

  25.        // request来了设置属性到当前线程的ThreadLocal

  26.        RequestContextHolder.setRequestAttributes(attributes);

  27.    }

  28.  

  29.    // 重写ServletRequestListener的方法,request销毁后调用

  30.    @Override

  31.    public void requestDestroyed(ServletRequestEvent requestEvent) {

  32.        ServletRequestAttributes attributes = null;

  33.        Object reqAttr = requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE);

  34.        if (reqAttr instanceof ServletRequestAttributes) {

  35.            attributes = (ServletRequestAttributes) reqAttr;

  36.        }

  37.        RequestAttributes threadAttributes = RequestContextHolder.getRequestAttributes();

  38.        if (threadAttributes != null) {

  39.            // We're assumably within the original request thread...

  40.            LocaleContextHolder.resetLocaleContext();

  41.            // request结束后删除当前线程的ThreadLocal

  42.            RequestContextHolder.resetRequestAttributes();

  43.            if (attributes == null && threadAttributes instanceof ServletRequestAttributes) {

  44.                attributes = (ServletRequestAttributes) threadAttributes;

  45.            }

  46.        }

  47.        if (attributes != null) {

  48.            attributes.requestCompleted();

  49.        }

  50.    }

知识点五:InheritableThreadLocal

可以被子线程继承的ThreadLocal

 
  1. public class Test5 extends Thread{

  2.  

  3.    static ThreadLocal<String> userId = new InheritableThreadLocal<>();

  4.  

  5.    public static void main(String[] args) throws InterruptedException {

  6.        userId.set("id in main thread");

  7.        Thread thread2 = new Thread(() -> {

  8.            System.out.println(userId.get());

  9.        });

  10.        Thread thread1 = new Thread(() -> {

  11.            System.out.println(userId.get());

  12.            thread2.start();

  13.            try {

  14.                thread2.join();

  15.            } catch (InterruptedException e) {

  16.                e.printStackTrace();

  17.            }

  18.        });

  19.        thread1.start();

  20.        thread1.join();

  21.    }

  22.  

  23. }

结果

 
  1. // 不仅是可以继承的,而且可以被孙子继承,可以一直传下去,传家宝。

  2. id in main thread

  3. id in main thread

实现是耦合在Thread类中的,当子线程初始化的时候,将父线程的inheritableThreadLocals设置到子线程中。比较简单,就不展开了。

 
  1. // java.lang.Thread

  2.    /*

  3.     * InheritableThreadLocal values pertaining to this thread. This map is

  4.     * maintained by the InheritableThreadLocal class.

  5.     */

  6.    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  7.       // java.lang.Thread

  8.       if (inheritThreadLocals && parent.inheritableThreadLocals != null)

  9.            this.inheritableThreadLocals =

  10.                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这里有一个坑。

ThreadLocal userId = InheritableThreadLocal.withInitial(()->"init id");

InheritableThreadLocal并没有重写withInitial方法,这样创建的ThreadLocal实质上不是一个InheritableThreadLocal对象,而是一个SuppliedThreadLocal对象。

 
  1.    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {

  2.        return new SuppliedThreadLocal<>(supplier);

  3.    }

  4.    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

  5.  

  6.        private final Supplier<? extends T> supplier;

  7.  

  8.        SuppliedThreadLocal(Supplier<? extends T> supplier) {

  9.            this.supplier = Objects.requireNonNull(supplier);

  10.        }

  11.  

  12.        @Override

  13.        protected T initialValue() {

  14.            return supplier.get();

  15.        }

  16.    }

在Spring中,大部分应用到ThreadLocal的地方都提供了InheritableThreadLocal的实现,可以通过配置启用。但是在我看来应用场景真的不多。无非就是下面的例子:

 
  1. new Thread(()->{

  2.      HttpServletRequest req = ((ServletRequestAttributes)

  3. RequestContextHolder.getRequestAttributes()).getRequest();

  4. }).start();

但是真正使用的时候我们会这样手动创建一个线程吗?一般都用线程池吧。如下:

 
  1. pool.execute(()->{

  2.     HttpServletRequest req = ((ServletRequestAttributes)

  3. RequestContextHolder.getRequestAttributes()).getRequest();

  4. });

线程池中的线程和当前线程非亲非故,怎么继承你的InheritableThreadLocal啊。。。

当项目中用到自定义线程池的时候,需要非常注意这些ThreadLocal对象的使用。因为在线程池中你是得不到ThreadLocal的值的。一个典型的例子是Hystrix的线程隔离,你必须清楚的知道,在Hystrix的线程池中是获取不到request线程的ThreadLocal的,否则坑就这么悄然而至。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值