ThreadLocal本地线程原理

ThreadLocal

与Synchronized对比

同样的两种解决数据安全的问题,synchronized (时间换空间)方式解决的是在多线程环境下解决共享数据的原子性问题。ThreadLocal(空间换时间)方式的细化到每一个线程内部,在一个线程内提供一份数据的副本。以此来保证线程安全的。因为线程计算机执行的最小单位。就不会存在数据问题了。

ThreadLocal关键源码

 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对象后,先进行set(),将此线程中想要共享的对象set到ThreadLocal中的ThreadLocalMap此Map类似HashMap,但是此map是由Thread类管理的。而且此map是懒加载。用到的时候在初始化。所以第一次set时,就需要调用createMap()。源码如下。

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

参数为当前线程t,想要设置的value。 初始化一个ThreadLocalMap。K 为ThreadLocal,K为先要设置的value。

  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

此处需要注意的是,这个ThreadLocalMap 与HashMap不一样的地方是此map中的entry类继承了WeakReference<ThreadLocal<?>> 。说明此处的K是弱引用。作用就是在gc的时候会回收entry中的K。避免内存溢出。后面会详细说。
这样就将本次线程中的共享变量设置好了。
如果取当前变量的话,就需要调用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();
    }

由于在同一线程内,那么获取到的ThreadLocalMap就是同一个。在根据当前线程的K(this即ThreadLocal)。去便利整个ThreadLocalMap,而不能直接获取。

  private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

这是一个正常的流程,但是这绝不是,ThreadLocal的魅力。
魅力1:
如果我们之前没有调用set(),方法,就直接get。正常逻辑是会get到一个null;但是ThreadLocal肯定会有不一样的地方。

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

第一次调用,ThreadLocalMap需要初始化。map会为空。调用setInitialValue();

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

我们第一次的value是在initialValue()方法得到的。

 protected T initialValue() {
        return null;
    }

我们可以在创建ThreadLocal实例时,通过匿名内部类重写initialValue()方法。就可以直接设置我们初始化时的第一个value值。

 private  static ThreadLocal<SimpleDateFormat>  simpleDateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("YYYY年MM月dd日  HH:mm:ss E");
        }
    };

这样就可以直接get到SimpleDateFormat的实例。并且是整个线程共享的。
魅力2;
ThreadLocalMap中的Entry继承了WeakReference<ThreadLocal<?>>,那么其中的K就为弱引用,其中的作用就是为了避免一部分内存泄露。
这里不防简单说一下硬,软,弱,虚引用的区别。
硬引用:

A a=new A();
B b=new B();

当a=null,b=null或当前方法执行完毕,gc会回收堆里边对应的对象。
或者

B b=new B();
A a=new A(b);
b=null;

当手动调用System.gc()时,b对象并不会被回收掉。因为a中还引用着b。
在这里插入图片描述

软引用

 //软引用  只有堆内存不够的时候,gc才会回收软引用对象   用作缓存,当内存不够时,可以清楚
//        SoftReference<byte[]> softReference = new SoftReference(new byte[1024 * 1024 * 10]);
//        System.gc();
//        Thread.sleep(3000);
//        System.out.println(softReference.get());
//        byte[] bytes = new byte[1024 * 1024 * 15];
//        System.out.println(softReference.get());

软引用是个对象,SoftReference。
在这里插入图片描述
手动gc并不会回收byte[],只有在内存不够的时候,gc才会回收软连接引用。所以,软引用都一般都用做缓存。

弱引用

 //弱引用  gc时,所有的弱引用都会被gc回收掉    ThreadLocal使用,为了避免内存泄漏
//        WeakReference<Date> date=new WeakReference<>(new Date());
//        System.out.println(date.get());
//        System.gc();
//        Thread.sleep(500);
//        System.out.println(date.get());

在这里插入图片描述
弱引用对象在遇见gc时就会被回收。而在ThreadLocal中就使用弱引用。

虚引用:

private static final List<Object> LIST = new ArrayList<>();
    private static final ReferenceQueue<Date> QUEUE = new ReferenceQueue<Date>();
  //虚引用  gc时会回收引用对象  创建时需要传一个queue  此用法用在堆外内存 ,删除后将地址传给队列,让队列去删除堆外内存的数据
        PhantomReference<Date> phantomReference=new PhantomReference<>(new Date(),QUEUE);
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    LIST.add(new byte[1024*1024]);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(phantomReference.get());
                }
            }
        }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            while(true) {
                Reference<? extends Date> poll = QUEUE.poll();
                poll.clear();
            }
        }
    }).start();
    }

简单理解虚引用 gc时会回收引用对象 创建时需要传一个queue 。此用法用在堆外内存 ,删除后将地址传给队列,让队列去删除堆外内存的数据。nio就使用虚引用。

此处重点关注弱引用。
因为我们在ThreadLocal中,弱引用的情形时这样的。
在这里插入图片描述

gc回收时,entry的K就会被回收。如果K为null,那么value也用不了。而且value使用的是硬引用,gc也回收不了,就会出现内存泄露的情况。所以,此是就需要手动的调用ThreadLocal的remove()方法。将entry中的key=null,value=null,gc就会回收这些对象,避免内存泄露的问题。如果没有remove的话,K的弱引用也会被回收,此时就会出现K=null的情况。但是ThreadLocal类中对K=null的情况有做优化。

例如在set的时候:

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

在set时,先遍历整个ThreadLocalMap,如果找到K对应的value直接覆盖之前的value,如果找到有k==null的情况,这肯定是弱引用产生的,那么就使用replaceStaleEntry方法调用expungeStaleEntry()将这个entry删除。 如果没有找到key,将entry删除后。就会new Entry();

在get的时候:

 private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

如果得到的对应位置的k==null 或者entry为null,就会通过getEntryAfterMiss方法调用expungeStaleEntries再一次遍历ThreadLocalMap将k=null的entry删除。方便gc回收。
在size=threshold时(扩容之前),也会先遍历ThreadLocalMap,将此k=null的值先删除掉,在判断是否扩容。
以上3种优化,均属于被动删除,就是在调用set,get方法时对map进行遍历,删除k为null的entry。避免内存泄露的情况。
但是,还是要在ThreadLocal用完后主动remove。才能有效避免内存泄露的情况。

在实际生产中,更多的是使用线程池管理线程,再加上ThreadLocal又经常会被static修饰。对象的生命周期过长还是会有很大的内存泄露的风险。另外,在没有主动remove的entry的,恰好有没有gc,就会拿到上一次请求存在ThreadLocal中的变量。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值