关于多线程:面试官看你简历说写精通ThreadLocal这几道题你都会吗

37次阅读

共计 12780 个字符,预计需要花费 32 分钟才能阅读完成。

问题


  1. 和 Synchronized 的区别
  2. 存储在 jvm 的哪个区域
  3. 真的只是以后线程可见吗
  4. 会导致内存透露么
  5. 为什么用 Entry 数组而不是 Entry 对象
  6. 你学习的开源框架哪些用到了 ThreadLocal
  7. ThreadLocal 里的对象肯定是线程平安的吗
  8. 口试题

一、概述


1、官网术语

ThreadLocal 类是用来提供线程外部的局部变量。让这些变量在多线程环境下拜访(get/set)时能保障各个线程里的变量绝对独立于其余线程内的变量。

2、大白话

ThreadLocal 是一个对于创立线程局部变量的类。

通常状况下,咱们创立的成员变量都是线程不平安的。因为他可能被多个线程同时批改,此变量对于多个线程之间彼此并不独立,是共享变量。而应用 ThreadLocal 创立的变量只能被以后线程拜访,其余线程无法访问和批改。也就是说:将线程公有化变成线程私有化。

二、利用场景


  • 每个线程都须要一个独享的对象(比方工具类,典型的就是 SimpleDateFormat,每次应用都 new 一个多节约性能呀,间接放到成员变量里又是线程不平安,所以把他用ThreadLocal 治理起来就完满了。)

比方:

/**
 * Description: SimpleDateFormat 就一份,不浪费资源。*
 * @author TongWei.Chen 2020-07-10 14:00:29
 */
public class ThreadLocalTest05 {public static String dateToStr(int millisSeconds) {Date date = new Date(millisSeconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }

    private static final ExecutorService executorService = Executors.newFixedThreadPool(100);

    public static void main(String[] args) {for (int i = 0; i < 3000; i++) {
            int j = i;
            executorService.execute(() -> {String date = dateToStr(j * 1000);
                // 从后果中能够看出是线程平安的,工夫没有反复的。System.out.println(date);
            });
        }
        executorService.shutdown();}
}

class ThreadSafeFormatter {public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

    // java8 的写法,装逼神器
//    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
//            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

仔细的敌人曾经发现了,这 TM 也是每个线程都创立一个 SimpleDateFormat 啊,跟间接在办法外部 new 没区别,错了,大错特错!1 个申请进来是一个线程,他可能贯通了 N 个办法,你这 N 个办法假如有 3 个都在应用 dateToStr(),你间接 new 的话会产生三个SimpleDateFormat 对象,而用 ThreadLocal 的话只会产生一个对象,一个线程一个。

  • 每个线程内须要保留全局变量(比方在登录胜利后将用户信息存到 ThreadLocal 里,而后以后线程操作的业务逻辑间接 get 取就完事了,无效的防止的参数来回传递的麻烦之处),肯定层级上缩小代码耦合度。

再细化一点就是:

  • 比方存储 交易 id 等信息。每个线程公有。
  • 比方 aop 里记录日志须要 before 记录申请 id,end 拿出申请 id,这也能够。
  • 比方 jdbc 连接池(很典型的一个 ThreadLocal 用法)
  • …. 等等 ….

三、外围常识


1、类关系

每个 Thread 对象中都持有一个 ThreadLocalMap 的成员变量。每个 ThreadLocalMap 外部又保护了 N 个 Entry 节点,也就是 Entry 数组,每个 Entry 代表一个残缺的对象,key 是 ThreadLocal 自身,value 是 ThreadLocal 的泛型值。

外围源码如下

// java.lang.Thread 类里持有 ThreadLocalMap 的援用
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;}

// java.lang.ThreadLocal 有外部动态类 ThreadLocalMap
public class ThreadLocal<T> {
    static class ThreadLocalMap {private Entry[] table;
        
        // ThreadLocalMap 外部有 Entry 类,Entry 的 key 是 ThreadLocal 自身,value 是泛型值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {super(k);
                value = v;
            }
        }
    }
}

2、类关系图

ThreadLocal内存结构图。

3、次要办法

  • initialValue:初始化。在 get 办法里懒加载的。
  • get:失去这个线程对应的 value。如果调用 get 之前没 set 过,则 get 外部会执行 initialValue 办法进行初始化。
  • set:为这个线程设置一个新值。
  • remove:删除这个线程对应的值,避免内存泄露的最佳伎俩。

3.1、initialValue

3.1.1、什么意思

见名知意,初始化一些 value(泛型值)。懒加载的。

3.1.2、触发机会

调用 get 办法之前没有调用 set 办法,则 get 办法外部会触发 initialValue,也就是说get 的时候如果没拿到货色,则会触发initialValue

3.1.3、补充阐明

  • 通常,每个线程最多调用一次此办法。然而如果曾经调用了 remove(),而后再次调用get() 的话,则能够再次触发initialValue
  • 如果要重写的话个别倡议采取匿名外部类的形式重写此办法,否则默认返回的是 null。

比方:

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
    @Override
    protected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
};
// Java8 的高逼格写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

3.1.4、源码

// 由子类提供实现。// protected 的含意就是交给子类干的。protected T initialValue() {return null;}

3.2、get

3.2.1、什么意思

获取以后线程下的 ThreadLocal 中的值。

3.2.2、源码

/**
 * 获取以后线程下的 entry 里的 value 值。* 先获取以后线程下的 ThreadLocalMap,* 而后以以后 ThreadLocal 为 key 取出 map 中的 value
 */
public T get() {
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取以后线程对应的 ThreadLocalMap 对象。ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此 ThreadLocalMap 下的 entry 对象,若 entry 也获取到了,那么间接获取 entry 对应的 value 返回即可。if (map != null) {
        // 获取此 ThreadLocalMap 下的 entry 对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若 entry 也获取到了
        if (e != null) {@SuppressWarnings("unchecked")
            // 间接获取 entry 对应的 value 返回。T result = (T)e.value;
            return result;
        }
    }
    // 若没获取到 ThreadLocalMap 或没获取到 Entry,则设置初始值。// 知识点:我早就说了,初始值办法是提早加载,只有在 get 才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就必定有值了,所以只会执行一次!!!return setInitialValue();}

3.3、set

3.3.1、什么意思

其实干的事和 initialValue 是一样的,都是 set 值,只是调用机会不同。set 是想用就用,api 摆在这里,你想用就调一下 set 办法。很自在。

3.3.2、源码

/**
 * 设置以后线程的线程局部变量的值
 * 实际上 ThreadLocal 的值是放入了以后线程的一个 ThreadLocalMap 实例中,所以只能在本线程中拜访。*/
public void set(T value) {
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取以后线程对应的 ThreadLocalMap 实例,留神这里是将 t 传进去了,t 是以后线程,就是说 ThreadLocalMap 是在线程里持有的援用。ThreadLocalMap map = getMap(t);
    // 若以后线程有对应的 ThreadLocalMap 实例,则将以后 ThreadLocal 对象作为 key,value 做为值存到 ThreadLocalMap 的 entry 里。if (map != null)
        map.set(this, value);
    else
        // 若以后线程没有对应的 ThreadLocalMap 实例,则创立 ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}

3.4、remove

3.4.1、什么意思

将以后线程下的 ThreadLocal 的值删除,目标是为了缩小内存占用。次要目标是避免内存透露。内存透露问题上面会说。

3.4.2、源码

/**
 * 将以后线程局部变量的值删除,目标是为了缩小内存占用。次要目标是避免内存透露。内存透露问题上面会说。*/
public void remove() {
    // 获取以后线程的 ThreadLocalMap 对象,并将其移除。ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 间接移除以以后 ThreadLocal 为 key 的 value
        m.remove(this);
}

4、ThreadLocalMap

为啥独自拿出来说下,我就是想强调一点:这个货色是归 Thread 类所有的。它的援用在 Thread 类里,这也证实了一个问题:ThreadLocalMap类外部为什么有 Entry 数组,而不是 Entry 对象?

因为你业务代码能 new 好多个 ThreadLocal 对象,各司其职。然而在一次申请里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不论你 new 几次 ThreadLocalThreadLocalMap 在一个线程里就一个,因为再说一次,ThreadLocalMap的援用是在 Thread 里的,所以它外面的 Entry 数组寄存的是一个线程里你 new 进去的多个 ThreadLocal 对象。

外围源码如下:

// 在你调用 ThreadLocal.get()办法的时候就会调用这个办法,它的返回是以后线程里的 threadLocals 的援用。// 这个援用指向的是 ThreadLocal 里的 ThreadLocalMap 对象
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

public class Thread implements Runnable {
    // ThreadLocal.ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

四、残缺源码


1、外围源码

// 本地线程。Thread:线程。Local:本地
public class ThreadLocal<T> {

    // 结构器
 public ThreadLocal() {}

    // 初始值,用来初始化值用的,比方:ThreadLocal<Integer> count = new ThreadLocal<>();
    // 你想 Integer value = count.get(); value++; 这样是报错的,因为 count 当初还没值,取出来的是个 null, 所以你须要先重写此办法为 value 赋上初始值,自身办法是 protected 也代表就是为了子类重写的。// 此办法是一个提早调用办法,在线程第一次调用 get 的时候才执行,上面具体分析源码就晓得了。protected T initialValue() {}

    // 创立 ThreadLocalMap,ThreadLocal 底层其实就是一个 map 来保护的。void createMap(Thread t, T firstValue) {}

    // 返回应当火线程对应的线程局部变量值。public T get() {}

    // 获取 ThreadLocalMap
 ThreadLocalMap getMap(Thread t) {}

    // 设置以后线程的线程局部变量的值
 public void set(T value) {}

    // 将以后线程局部变量的值删除,目标是为了缩小内存占用。其实当线程完结后对应该线程的局部变量将主动被垃圾回收,所以无需咱们调用 remove,咱们调用 remove 无非也就是放慢内存回收速度。public void remove() {}

    // 设置初始值,调用 initialValue
 private T setInitialValue() {}

    // 动态外部类,一个 map 来保护的!!!static class ThreadLocalMap {
  
        // ThreadLocalMap 的动态外部类,继承了弱援用,这正是不会造成内存透露根本原因
        // Entry 的 key 为 ThreadLocal 并且是弱援用。value 是值
  static class Entry extends WeakReference<ThreadLocal<?>> {}}

}

2、set()

/**
 * 设置以后线程的线程局部变量的值
 * 实际上 ThreadLocal 的值是放入了以后线程的一个 ThreadLocalMap 实例中,所以只能在本线程中拜访。*/
public void set(T value) {
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取以后线程对应的 ThreadLocalMap 实例
    ThreadLocalMap map = getMap(t);
    // 若以后线程有对应的 ThreadLocalMap 实例,则将以后 ThreadLocal 对象作为 key,value 做为值存到 ThreadLocalMap 的 entry 里。if (map != null)
        map.set(this, value);
    else
        // 若以后线程没有对应的 ThreadLocalMap 实例,则创立 ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}

3、getMap()

// 在你调用 ThreadLocal.get()办法的时候就会调用这个办法,它的返回是以后线程里的 threadLocals 的援用。// 这个援用指向的是 ThreadLocal 里的 ThreadLocalMap 对象
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

public class Thread implements Runnable {
    // ThreadLocal.ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

4、map.set()

// 不多 BB,就和 HashMap 的 set 一个情理,只是赋值 key,value。// 须要留神的是这里 key 是 ThreadLocal 对象,value 是值
private void set(ThreadLocal<?> key, Object value) {}

5、createMap()

/**
 * 创立 ThreadLocalMap 对象。* t.threadLocals 在下面的 getMap 中具体介绍了。此处不 BB。* 实例化 ThreadLocalMap 并且传入两个值,一个是以后 ThreadLocal 对象一个是 value。*/
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLocalMap 结构器。ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 重点看这里!!!!!!// new 了一个 ThreadLocalMap 的外部类 Entry,且将 key 和 value 传入。// key 是 ThreadLocal 对象。table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

/**
 * 到这里敌人们应该水落石出了,其实 ThreadLocal 就是外部保护一个 ThreadLocalMap,* 而 ThreadLocalMap 外部又保护了一个 Entry 对象。Entry 对象是 key-value 模式,* key 是 ThreadLocal 对象,value 是传入的 value
 * 所以咱们对 ThreadLocal 的操作其实都是对外部的 ThreadLocalMap.Entry 的操作
 * 所以保障了线程之前互不烦扰。*/

6、get()

/**
 * 获取以后线程下的 entry 里的 value 值。* 先获取以后线程下的 ThreadLocalMap,* 而后以以后 ThreadLocal 为 key 取出 map 中的 value
 */
public T get() {
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取以后线程对应的 ThreadLocalMap 对象。ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此 ThreadLocalMap 下的 entry 对象,若 entry 也获取到了,那么间接获取 entry 对应的 value 返回即可。if (map != null) {
        // 获取此 ThreadLocalMap 下的 entry 对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若 entry 也获取到了
        if (e != null) {@SuppressWarnings("unchecked")
            // 间接获取 entry 对应的 value 返回。T result = (T)e.value;
            return result;
        }
    }
    // 若没获取到 ThreadLocalMap 或没获取到 Entry,则设置初始值。// 知识点:我早就说了,初始值办法是提早加载,只有在 get 才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就必定有值了,所以只会执行一次!!!return setInitialValue();}

7、setInitialValue()

// 设置初始值
private T setInitialValue() {
    // 调用初始值办法,由子类提供。T value = initialValue();
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取 map
    ThreadLocalMap map = getMap(t);
    // 获取到了
    if (map != null)
        // set
        map.set(this, value);
    else
        // 没获取到。创立 map 并赋值
        createMap(t, value);
    // 返回初始值。return value;
}

8、initialValue()

// 由子类提供实现。// protected
protected T initialValue() {return null;}

9、remove()

/**
 * 将以后线程局部变量的值删除,目标是为了缩小内存占用。* 其实当线程完结后对应该线程的局部变量将主动被垃圾回收,所以无需咱们调用 remove,咱们调用 remove 无非也就是放慢内存回收速度。*/
public void remove() {
    // 获取以后线程的 ThreadLocalMap 对象,并将其移除。ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

10、小结

只有捋分明如下几个类的关系,ThreadLocal将变得 so easy!

ThreadThreadLocalThreadLocalMapEntry

一句话总结就是:Thread保护了 ThreadLocalMap,而ThreadLocalMap 里保护了 Entry,而Entry 里存的是以 ThreadLocal 为 key,传入的值为 value 的键值对。

五、答疑(面试题)


1、和 Synchronized 的区别

问:他和线程同步机制(如:Synchronized)提供一样的性能,这个很吊啊。

答:放屁!同步机制保障的是多线程同时操作共享变量并且能正确的输入后果。ThreadLocal 不行啊,他把共享变量变成线程公有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量 count++ 的时候带上 synchronized 即可解决。ThreadLocal 的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。

2、存储在 jvm 的哪个区域

问:线程公有,那么就是说 ThreadLocal 的实例和他的值是放到栈上咯?

答:不是。还是在堆的。ThreadLocal 对象也是对象,对象就在堆。只是 JVM 通过一些技巧将其可见性变成了线程可见。

3、真的只是以后线程可见吗

问:真的只是以后线程可见吗?

答:貌似不是,貌似通过 InheritableThreadLocal 类能够实现多个线程拜访 ThreadLocal 的值,然而我没钻研过,晓得这码事就行了。

4、会导致内存透露么

问:会导致内存透露么?

答:剖析一下:

  • 1、ThreadLocalMap.Entry的 key 会内存透露吗?
  • 2、ThreadLocalMap.Entry的 value 会内存透露吗?

先看下 key-value 的外围源码

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

先看继承关系,发现是继承了弱援用,而且 key 间接是交给了父类解决super(key),父类是个弱援用,所以 key 齐全不存在内存透露问题,因为他不是强援用,它能够被 GC 回收的。

弱援用的特点:如果这个对象只被弱援用关联,没有任何强援用关联,那么这个对象就能够被 GC 回收掉。弱援用不会阻止 GC 回收。这是 jvm 常识。

再看 value,发现 value 是个强援用,然而想了下也没问题的呀,因为线程终止了,我管你强援用还是弱援用,都会被 GC 掉的,因为援用链断了(jvm 用的可达性分析法,线程终止了,根节点就断了,上面的都会被回收)。

这么剖析一点故障都没有,然而忘了一个次要的角色,那就是 线程池,线程池的存在外围线程是不会销毁的,只有创立进去他会重复利用,生命周期不会完结掉,然而 key 是弱援用会被 GC 回收掉,value 强援用不会回收,所以造成了如下局面:

Thread->ThreadLocalMap->Entry(key 为 null)->value

因为 value 和 Thread 还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无奈回收,晚上内存透露,工夫久了必然 OOM。

解决方案 ThreadLocal 曾经为咱们想好了,提供了 remove() 办法,这个办法是将 value 移出去的。所以用完后记得remove()

5、为什么用 Entry 数组而不是 Entry 对象

这个其实次要想考 ThreadLocalMap 是在 Thread 里持有的援用。

问:ThreadLocalMap外部的 table 为什么是数组而不是单个对象呢?

答:因为你业务代码能 new 好多个 ThreadLocal 对象,各司其职。然而在一次申请里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不论你 new 几次 ThreadLocalThreadLocalMap 在一个线程里就一个,因为 ThreadLocalMap 的援用是在 Thread 里的,所以它外面的 Entry 数组寄存的是一个线程里你 new 进去的多个 ThreadLocal 对象。

6、你学习的开源框架哪些用到了 ThreadLocal

Spring 框架。

DateTimeContextHolder

RequestContextHolder

7、ThreadLocal 里的对象肯定是线程平安的吗

未必,如果在每个线程中 ThreadLocal.set() 进去的货色原本就是多线程共享的同一个对象,比方 static 对象,那么多个线程的 ThreadLocal.get() 获取的还是这个共享对象自身,还是有并发拜访线程不平安问题。

8、口试题

问:上面这段程序会输入什么?为什么?

public class TestThreadLocalNpe {private static ThreadLocal<Long> threadLocal = new ThreadLocal();

    public static void set() {threadLocal.set(1L);
    }

    public static long get() {return threadLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {new Thread(() -> {set();
            System.out.println(get());
        }).start();
        // 目标就是为了让子线程先运行完
        Thread.sleep(100);
        System.out.println(get());
    }
}

答:

1
Exception in thread "main" java.lang.NullPointerException
 at com.chentongwei.study.thread.TestThreadLocalNpe.get(TestThreadLocalNpe.java:16)
 at com.chentongwei.study.thread.TestThreadLocalNpe.main(TestThreadLocalNpe.java:26)

为什么?

为什么输入个 1,而后空指针了?

首先输入 1 是没任何问题的,其次主线程空指针是为什么?

如果你这里答复

1
1

那我祝贺你,你连 ThreadLocal 都不晓得是啥,这显著两个线程,子线程和主线程。子线程设置 1,主线程必定拿不到啊,ThreadLocal和线程是嘻嘻相干的。这个不多费口舌。

说说为什么是空指针?

因为你 get 办法用的 long 而不是 Long,那也应该返回 null 啊,大哥,long 是根本类型,默认值是 0,没有 null 这一说法。ThreadLocal里的泛型是 Long,get 却是根本类型,这须要拆箱操作的,也就是会执行 null.longValue() 的操作,这绝逼空指针了。

看似一道 Javase 的根底题目,实则暗藏了很多常识。

六、ThreadLocal 工具类


package com.duoku.base.util;

import com.google.common.collect.Maps;
import org.springframework.core.NamedThreadLocal;

import java.util.Map;

/**
 * Description:
 *
 * @author TongWei.Chen 2019-09-09 18:35:30
 */
public class ThreadLocalUtil {private static final ThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("xxx-threadlocal") {
        @Override
        protected Map<String, Object> initialValue() {return Maps.newHashMap();
        }
    };

    public static Map<String, Object> getThreadLocal(){return threadLocal.get();
    }
    
    public static <T> T get(String key) {Map map = threadLocal.get();
        // todo:copy a new one
        return (T)map.get(key);
    }

    public static <T> T get(String key,T defaultValue) {Map map = threadLocal.get();
        return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
    }

    public static void set(String key, Object value) {Map map = threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) {Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() {threadLocal.remove();
    }

}

写在最初

欢送大家关注我的公众号【惊涛骇浪如码】,海量 Java 相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。

感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!

正文完
 0