ThreadLocal原理及使用

ThreadLocal 两大使用场景

文章首发于公众号,欢迎订阅
ThreadLocal原理及使用_第1张图片

场景一:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormatRandom)。

public class ThreadLocalDemo1 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int end = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo1().date(end);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    // 写法一
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    // 写法二
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

场景二:每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取用户信息,该信息在本线程执行的各方法中保持不变),可以让不同方法直接使用,却不想被多线程共享(因为不同线程获取到的用户信息不一样),避免参数传递的麻烦。

public class ThreadLocalDemo2 {

    public static void main(String[] args) {
        new Service1().process("");

    }
}

class Service1 {

    public void process(String name) {
        User user = new User("feichaoyu");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {

    String name;

    public User(String name) {
        this.name = name;
    }
}

可以得出 ThreadLocal 的两大作用:

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)。
  • 在任何方法中都可以轻松获取到该对象。

使用 ThreadLocal 的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率。
  • 更高效地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用 ThreadLocal 可以节省内存和开销。
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传同样的参数。Threadlocal 使得代码耦合度更低,更优雅。

ThreadLocal 的实现原理

概述

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals , 它们都是 ThreadLocalMap 类型的变量 。在默认情况下, 每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocalset 或者 get 方法时才会创建它们 。 其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面 。 也就是说 , ThreadLocal 类型的本地变量存放在具体的线程内存空间中 。 ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来 , 当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用 。 如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面 ,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法 ,从当前线程的 threadLocals 里面删除该本地变量 。

get 方法源码

get 方法:

public T get() {
    // 获取当前线程t
    Thread t = Thread.currentThread();
    // 获取当前线程t持有的map
    ThreadLocalMap map = getMap(t);

    // 如果map不为null,返回其键值对中保存的value
    if (map != null) {
        // this指的是当前ThreadLocal,通过当前ThreadLocal获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 如果Entry存在,通过Entry获取值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }

    // 1.map为空
    // 2.map不为空,但是还没有存储当前的ThreadLocalMap对象
    // 则执行以下逻辑
    return setInitialValue();
}

setInitialValue 方法:

private T setInitialValue() {
    // 可以自己设置初值,否则默认为null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果map不为空,则更新值
    if (map != null)
        map.set(this, value);
    // 否则创建新的map
    else
        createMap(t, value);
    return value;
}

createMap 方法:

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

set 方法源码

set 方法:

public void set(T value) {
    // 获取当前线程t
    Thread t = Thread.currentThread();
    // 获取当前线程t持有的map
    ThreadLocalMap map = getMap(t);
    // 如果map不为空,则更新值
    if (map != null)
        map.set(this, value);
    // 否则创建新的map
    else
        createMap(t, value);
}

remove 方法源码

remove 方法:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

remove(this)方法:

private void remove(ThreadLocal<?> key) {
    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)]) {
        // 找到要删除的key
        if (e.get() == key) {
            // 去除key的软引用
            e.clear();
            // 
            expungeStaleEntry(i);
            return;
        }
    }
}

expungeStaleEntry 方法:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 注意到这里
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

expungeStaleEntry方法调用前有这么一行代码e.clear();,该行代码去除了 key 的软引用,那么在expungeStaleEntry方法中我们注意到ThreadLocal k = e.get();,此时 knull,符合下面的 if 判断,会将e.value置为 null,完成了整个键值对的释放,此时就不会内存泄漏了。

ThreadLocal 原理总结

  1. 每个 Thread 维护着一个 ThreadLocalMap 的引用
  2. ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储
  3. 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值是传递进来的对象
  4. 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 获取值,key 是 ThreadLocal 对象
  5. ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

ThreadLocal 的内存泄漏

内存泄漏是指,某个对象不再使用,但是占用的内存无法回收。

ThreadLocalMap 中有一个 Entry 内部类,它的代码如下:

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

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

Entry 中保存了键和值,键使用的是 WeakReference 弱引用(可以自动被 GC 回收),值使用的是强引用(无法自动被 GC 回收)。

正常情况下,当线程停止,保存在 ThreadLocal 中的 value 会被垃圾回收,因为没有任何强引用了。但是如果线程不停止,那么 value 就无法被垃圾回收。

链路:Thread -> ThreadLocalMap -> Entry -> Value

因为 valueThread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现 OOM。

因此阿里规约中指出,在使用完 ThreadLocal 后,应该调用 remove 方法。

原理是在 remove 方法中,会调用 expungeStaleEntry 方法,这个方法的意思是清除老的Entry

if (k == null) {
    e.value = null;
}

我创建了一个免费的知识星球,用于分享知识日记,欢迎加入!

ThreadLocal原理及使用_第2张图片

你可能感兴趣的:(Java并发)