threadlocal

什么是ThreadLocal?

线程本地存储技术,能够做到线程隔离

为什么要使用ThreadLocal?

同一个线程里面,可以跨对象的去共享数据,同时保证线程安全,每个线程只访问自己的数据

怎么使用?

ThreadLocal threadLocal = new ThreadLocal();

threadLocal.set(value);   存储线程隔离的数据

threadLocal.get();   获取当前线程存储的数据

使用场景

如果想在同一个对象中,跨多个对象的去共享数据,同时保证线程安全的情况下,使用ThreadLocal

比如:

用户登录成功后,需要将登录用户信息保存起来,以方便在系统中的任何地方都可以使用到,那么此时就可以使用ThreadLocal来实现。例如:Spring Security中的ThreadLocalSecurityContextHolderStrategy类

比如

在Tomcat中,如果我们想在 filter,controller,service之间方便的去共享数据,并且保证每个请求只能访问自己的数据,此时可以使用ThreadLocal

工作原理

每个线程对象中都有一个自己的 ThreadLocalMap 对象,每个线程负责把自己的数据存到ThreadLocalMap 对象中,如上图,tomcat给每个请求分配一个线程(调用threadLocal.set(user1)在当前线程中存放user1数据),每个线程中有一个成员变量 threadLocals,成员变量指向每个线程中的 ThreadLocalMap 对象,当调用threadLocal.get()方法时,就可以取出当前线程的资源

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();

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);
    }

ThreadLocal为什么会内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。根本原因是因为每个线程中的 ThreadLocalMap 生命周期和线程的生命周期是一样长的

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

如何解决内存泄漏

在最后要被线程池回收之前,使用 threadLocal.remove() 方法,把当前threadlocalmap中的数据清空掉

key为什么要设置成弱引用?

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry(Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。)内存泄漏完全没必要过于担心。

为什么value不是弱引用

先说答案:假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。

        Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
        WeakReference<Integer> key = new WeakReference<>(1);
        WeakReference<Integer> value = new WeakReference<>(300);
        map.put(key,value);
        System.out.println("put success");
        Thread.sleep(1000);
        System.gc();
        System.out.println("get " + map.get(key).get());

//        put success
//        get null

使用ThreadLocal的目的就是要把这个value存储到当前线程中,并且希望在你需要的时候直接从当前线程中拿出来,那么意味着你的value除了当前线程对它持有强引用外,理论上来说,不应该再有其他强引用,否则你也不会把value存储进当前线程。但是一旦你把本应该强引用的value设计成了弱引用,那么只要jvm执行一次gc操作,你的value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。

使用threadlocal的缺点

脏读(在一个线程中读取到了不属于自己的信息就叫做脏读)

脏读产生原因:线程池复用了线程,和这个线程相关的静态属性也复用,所以就导致了脏读

    //脏读问题
    private static ThreadLocal<String> threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        ThreadPoolExecutor executor=new ThreadPoolExecutor(1,1,0,
                TimeUnit.SECONDS,new LinkedBlockingQueue<>(1000));
        for (int i=0;i<2;i++){
            executor.submit(new MyThreadLocal());
        }
    }
    static class MyThreadLocal extends Thread{
         //静态变量被复用
        private static boolean flag=false;

        @Override
        public void run() {
            String tname=this.getName();
            if(!flag){
                //第一次执行
                threadLocal.set(tname);
                System.out.println(tname+"设置了"+tname);
                flag=true;
            }
            System.out.println(tname+"得到了"+threadLocal.get());

        }
    }

解决脏读问题:

a.避免使用静态变量

b.使用完之后,执行remove移除

内存溢出(配合线程池使用的时候才会出现)
当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就是内存溢出

//设置最大内存的是5M
public class ThreadLocalDemo11 {
    private static ThreadLocal<MyThreadLocal>threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000));
        for(int i=0;i<10;i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    MyThreadLocal myThreadLocal=new MyThreadLocal();
                    threadLocal.set(myThreadLocal);
                    System.out.println(Thread.currentThread().getName()+
                            "设置了值");
                }
            });
        }
    }
    static class MyThreadLocal{
        private byte[]bytes=new byte[1*1024*1024];
    }
}

解决内存溢出:使用remove移除
内存溢出原因(配合线程池使用的时候才会出现内存溢出):
1.线程池是长生命周期,使用完后不会主动释放资源
2.Thread–>ThreadLocalMap–>Entry[ ] key,value(1MB资源) key是弱引用,value是强引用,垃圾回收器不会回收value资源
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值