详解Threadlocal原理与用法

Threadlocal的原理与用法

北京的冬天来的特别快,这一周研究了一下Threadlocal,正好周天写下这篇博客,本文将从一下几个方面介绍Threadlocal
1 Threadlocal是什么?
2 Threadlocal源码解析
3 Threadlocal使用示例
4 Threadlocal为什么会内存泄漏?

一 Threadlocal是什么?
ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢? API是这样介绍它的:
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).
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。
二 Threadlocal源码解析
ThreadLocal定义了四个方法:
get():返回此线程局部变量的当前线程副本中的值。
initialValue():返回此线程局部变量的当前线程的“初始值”。
remove():移除此线程局部变量当前线程的值。
set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
在这里插入图片描述
除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。 对于ThreadLocal需要注意的有两点:

ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

下图是Thread、ThreadLocal、ThreadLocalMap的关系
在这里插入图片描述
我们直接看最常用的set操作:
在这里插入图片描述在这里插入图片描述在这里插入图片描述你会看到,set需要首先获得当前线程对象Thread;
然后取出当前线程对象的成员变量ThreadLocalMap;
如果ThreadLocalMap存在,那么进行KEY/VALUE设置,KEY就是ThreadLocal;
如果ThreadLocalMap没有,那么创建一个;
说白了,当前线程中存在一个Map变量,KEY是ThreadLocal,VALUE是你设置的值。
看一下get操作:
在这里插入图片描述
这里其实揭示了ThreadLocalMap里面的数据存储结构,从上面的代码来看,ThreadLocalMap中存放的就是Entry,Entry的KEY就是ThreadLocal,VALUE就是值。
ThreadLocalMap.Entry:
在这里插入图片描述
initialValue()
返回该线程局部变量的初始值。
在这里插入图片描述该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。
remove()
将当前线程局部变量的值删除。
在这里插入图片描述该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。

3 Threadlocal使用示例

public class SeqCount {

    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
        // 实现initialValue()
        public Integer initialValue() {
            return 0;
        }
    };

    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);

        return seqCount.get();
    }

    public static void main(String[] args){
        SeqCount seqCount = new SeqCount();

        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

    private static class SeqThread extends Thread{
        private SeqCount seqCount;

        SeqThread(SeqCount seqCount){
            this.seqCount = seqCount;
        }

        public void run() {
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
            }
        }
    }
}

运行结果:
在这里插入图片描述
从运行结果可以看出,ThreadLocal确实是可以达到线程隔离机制,确保变量的安全性。这里我们想一个问题,在上面的代码中ThreadLocal的initialValue()方法返回的是0,加入该方法返回得是一个对象呢,会产生什么后果呢?例如:

 A a = new A();
    private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
        // 实现initialValue()
        public A initialValue() {
            return a;
        }
    };

    class A{
        // ....
    }

我写了示例代码做了一个测试
在这里插入图片描述在这里插入图片描述
运行结果
在这里插入图片描述
很显然,在这里,并没有通过ThreadLocal达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼?

虽然,ThreadLocal让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象呢?这个时候ThreadLocal就失效了。仔细观察下图中的代码,你会发现,threadLocal在初始化时返回的都是同一个对象a!
下面再演示一个使用示例:
场景

本文应用ThreadLocal的场景:在调用API接口的时候传递了一些公共参数,这些公共参数携带了一些设备信息,服务端接口根据不同的信息组装不同的格式数据返回给客户端。假定服务器端需要通过设备类型(device)来下发下载地址,当然接口也有同样的其他逻辑,我们只要在返回数据的时候判断好是什么类型的客户端就好了。如下:
场景一

请求
在这里插入图片描述
返回
在这里插入图片描述场景二

请求
在这里插入图片描述
返回
在这里插入图片描述
实现

首先准备一个BaseSigntureRequest类用来存放公共参数

1public class BaseSignatureRequest {
 2    private String device;
 3
 4    public String getDevice() {
 5        return device;
 6    }
 7
 8    public void setDevice(String device) {
 9        this.device = device;
10    }
11}

然后准备一个static的ThreadLocal类用来存放ThreadLocal,以便存储和获取时候的ThreadLocal一致。

1public class ThreadLocalCache {
2    public static ThreadLocal<BaseSignatureRequest> 
3     baseSignatureRequestThreadLocal = new ThreadLocal<>();
4}

然后编写一个Interceptor,在请求的时候获取device参数,存入当前线程的ThreadLocal中。这里需要注意的是,重写了afterCompletion方法,当请求结束的时候把ThreadLocal remove,移除不必须要键值对。

1public class ParameterInterceptor implements HandlerInterceptor {
 2    @Override
 3    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
 4                             Object handler) throws Exception {
 5        String device = request.getParameter("device");
 6        BaseSignatureRequest baseSignatureRequest = new BaseSignatureRequest();
 7        baseSignatureRequest.setDevice(device);
 8        ThreadLocalCache.baseSignatureRequestThreadLocal.set(baseSignatureRequest);
 9        return true;
10    }
11
12    @Override
13    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
14                                Object handler, Exception ex) throws Exception {
15        ThreadLocalCache.baseSignatureRequestThreadLocal.remove();
16    }
17
18    @Override
19    public void postHandle(HttpServletRequest httpServletRequest,
20                           HttpServletResponse httpServletResponse, 
21                           Object o, ModelAndView modelAndView) throws Exception {
22
23    }
24}

当然需要在spring里面配置interceptor

1    <mvc:interceptors>
2        <mvc:interceptor>
3            <mvc:mapping path="/api/**"/>
4            <bean class="life.majiang.ParameterInterceptor"></bean>
5        </mvc:interceptor>
6    </mvc:interceptors>

最后在Converter里面转换实体的时候直接使用即可,这样就大功告成了。

 1public class UserConverter {
 2    public static ResultDO toDO(User user) {
 3        ResultDO resultDO = new ResultDO();
 4        resultDO.setUser(user);
 5        BaseSignatureRequest baseSignatureRequest = ThreadLocalCache.baseSignatureRequestThreadLocal.get();
 6        String device = baseSignatureRequest.getDevice();
 7        if (StringUtils.equals(device, "ios")) {
 8            resultDO.setLink("https://itunes.apple.com/us/app/**");
 9        } else {
10            resultDO.setLink("https://play.google.com/store/apps/details?id=***");
11        }
12        return resultDO;
13    }

4 Threadlocal为什么会内存泄漏?
在这里插入图片描述 在JAVA里面,存在强引用、弱引用、软引用、虚引用。这里主要谈一下强引用和弱引用。

强引用,就不必说了,类似于:

A a = new A();

B b = new B();

考虑这样的情况:

C c = new C(b);

b = null;

考虑下GC的情况。要知道b被置为null,那么是否意味着一段时间后GC工作可以回收b所分配的内存空间呢?答案是否定的,因为即便b被置为null,但是c仍然持有对b的引用,而且还是强引用,所以GC不会回收b原先所分配的空间!既不能回收利用,又不能使用,这就造成了内存泄露。

那么如何处理呢?

可以c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)

分析到这里,我们可以得到:

在这里插入图片描述这里我们思考一个问题:ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?

首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
那么如何有效的避免呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值