如何更好的使用ThreadLocal?

ThreadLocal

使用场景一

每个线程需要一个独享的对象(多数为工具类,例如SimpleDateFormat、Random);

  1. 每个Thread内有自己的实例,不共享;
  2. 如果其他线程要使用,获取ThreadLocal达到复制的效果;

需要重写initialValue方法,返回一个新对象;

第一次get的时候就初始化对象,这个初始化时机由我们控制,所以重写initialValue方法

使用场景二

每个线程内需要保存全局变量(例如拦截器截取用户信息),可以让不同方法使用,避免参数传递;

  1. 在同一个线程中信息相同,不同线程中信息不同(可以使用线程安全的map来解决,但是有性能开销);
  2. 使用ThreadLocal实例的get获取自己set的对象,避免参数传递;

初始化时机不受我们控制,直接set到ThreadLocal中,进行get即可

作用

  1. 某个需要用到的对象在线程间隔离(每个线程都有自己独享的对象);
  2. get轻松获取对象;

好处

  1. 线程安全
  2. 不需要加锁,效率高
  3. 利用内存,节省性能开销
  4. 避免传参麻烦

原理

每个Thread对象中都持有一个ThreadLocalMap(key-value)成员变量;

//Thread中有ThreadLocalMap,来存储多个ThreadLocal
ThreadLocal.ThreadLocalMap threadLocals = null;

方法

1. initialValue(初始化)

  1. initialValue(延迟加载,在调用get的时候才会触发)

  2. 如果在之在第一次调用get之前进行了set,就不会触发initialValue方法赋初始值;

  3. 每个线程最多调用一次这个方法,但如果调用了remove方法,再次调用get,就可以再次触发initialValue;

  4. 如果不重写initialValue方法,这个方法会返回null,一般使用匿名内部类重写initialValue;

//get方法
public T get() {
  //获得当前线程
    Thread t = Thread.currentThread();
  //获取localmap
    ThreadLocalMap map = getMap(t);
  //之前调用了set方法,map就不为空
    if (map != null) {
      //把当前threadlocal作为key,获取value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
          //拿到set进入的对象
            T result = (T)e.value;
            return result;
        }
    }
  //调用setInitialValue
    return setInitialValue();
}

 private T setInitialValue() {
   //调用initialValue获取对象
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

2. set(为线程设置值)

//存入对象,也就是value
public void set(T value) {
  //获取当前线程
    Thread t = Thread.currentThread();
  //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
  //如果不为null,就set进行覆盖
    if (map != null)
        map.set(this, value);
  //如果是null,就创建一个map
    else
        createMap(t, value);
}

3. get(得到线程对应的value,第一次调用get会调用initialValue)

先取到当前线程的ThreadLocalMap,然后调用map.getEntry,将当前this(ThreadLocal)传入,获取到ThreadLocal对应的value;

//get中调用getEntry	
private Entry getEntry(ThreadLocal<?> key) {
  //获取key的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
  //找到数组元素
    Entry e = table[i];
  //返回value
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

4. remove(删除对应线程的value)

public void remove() {
  //先拿到ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
      //传入对应的ThreadLocal进行删除
        m.remove(this);
}

ThreadLocalMap(Thread中的成员变量,存储多个ThreadLocal)

//键值对数组
//key是threadLocal,value是存储的对象或者变量
private Entry[] table;

哈希冲突解决(线性探测法)

线性探测法:发送hash冲突找下一个空位置进行存储;

Tips:

  • setInitialValue和直接set都是调用的map.set(this,value)进行设置值;
  • 最后都会存储为一个Entry,只是起点和入口不一样;

ThreadLocal注意点

  • 内存泄露(对象没有用了,但是内存不能被回收);
//ThreadLocal——ThreadLocalMap——Entry(键值对数组)
//继承了WeakReference弱引用
//弱引用是内存不足的时候,会被GC

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

Entry构造器中key使用的是弱引用value使用的强引用,这就导致value还存在强引用无法被回收,但是key是弱引用可以被回收,发送内存泄露;
如何更好的使用ThreadLocal?_第1张图片
JDK已经考虑了这个问题,在set、remove、rehash中会扫描key为null的Entry,并且把key对应的value设为null,帮助GC;但是不调用这些方法,就无法回收value

如何避免内存泄露

在使用完ThreadLocal后,应该调用remove方法进行清空;

你可能感兴趣的:(JAVA基础)