Java笔试面试-ThreadLocal

ThreadLocal

  ThreadLocal 诞生于 JDK 1.2,用于解决多线程间的数据隔离问题。也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本。

ThreadLocal 作用:

  • ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理;
  • 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal 来实现。

其中,ThreadLocal 也被用在很多大型开源框架中,比如 Spring 的事务管理器,还有 Hibernate 的 Session 管理等。
ThreadLocal 使用
ThreadLocal 基本使用
  ThreadLocal 常用方法有 set(T)、get()、remove() 等,具体使用请参考以下代码。

ThreadLocal threadLocal = new ThreadLocal();
// 存值
threadLocal.set(Arrays.asList("大冰", "Java 笔试面试题"));
// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());
//删除值
threadLocal.remove();
System.out.println(threadLocal.get());

以上程序执行结果如下:

2

[大冰, Java 笔试面试题]

null

ThreadLocal 所有方法,如下图所示:

ThreadLocal 数据共享

  ThreadLocal 设计的初衷是解决线程间信息隔离的,所有ThreadLocal 也能实现线程间的信息共享,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,具体代码如下:

ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("大冰");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();

以上程序执行结果如下:

大冰

从以上代码可以看出,主线程和新创建的线程之间实现了信息共享。

ThreadLocal 内存溢出

下面我们用代码实现 ThreadLocal 内存溢出的情况,请参考以下代码。

class ThreadLocalTest {
    static ThreadLocal threadLocal = new ThreadLocal();
    static Integer MOCK_MAX = 10000;
    static Integer THREAD_MAX = 100;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
        for (int i = 0; i < THREAD_MAX; i++) {
            executorService.execute(() -> {
                threadLocal.set(new ThreadLocalTest().getList());
                System.out.println(Thread.currentThread().getName());
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
    List getList() {
        List list = new ArrayList();
        for (int i = 0; i < MOCK_MAX; i++) {
            list.add("Version:JDK 8");
            list.add("ThreadLocal");
            list.add("Author:大冰");
            list.add("DateTime:" + LocalDateTime.now());
            list.add("Test:ThreadLocal OOM");
        }
        return list;
    }
}

设置 JVM(Java 虚拟机)启动参数 -Xmx=100m (最大运行内存 100 M),运行程序不久后就会出现如下异常:

内存溢出原理分析

在开始之前,先来看下 ThreadLocal 是如何存储数据的。

首先,找到 ThreadLocal.set() 的源码,代码如下(此源码基于 JDK 8):

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 对象,然后再执行 ThreadLocalMap.set() 方法,进而打开此方法的源码,代码如下:

private void set(ThreadLocal<?> key, Object value) {
    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();
}

从整个代码可以看出,首先 ThreadLocal 并不存储数据,而是把当前线程作为 key 存储到 ThreadLocalMap 中,其中 value 值就是传递过来实际要存储的值。而 ThreadLocalMap 本身使用的内部存储结构为 Entry,这样就可以把 ThreadLocal 和要存储的值以 K,V 的形式关联起来了。

看到这里我们就明白了,之所以 ThreadLocal 会造成内存溢出,是因为存储的值的生命周期是和当前线程绑定的(相同的)。由于当前线程是以 key 的形式和 ThreadLocal 的 value 共同存储在 ThreadLocalMap 中,又由于当前线程一直没有被释放,因而内存一直会增加,直到出现内存溢出。

ThreadLocal、Thread、ThreadLocalMap、Entry 之间的关系如下图所示:

ThreadLocal 的正确使用方法

  既然已经了解了 ThreadLocal 内存溢出的原因,那解决的办法就很简单了,只需要在使用完 ThreadLocal 之后,把内容删除掉(remove)就可以,因此 ThreadLocal 完整的正确使用代码如下:

class ThreadLocalTest {
    static ThreadLocal threadLocal = new ThreadLocal();
    static Integer MOCK_MAX = 10000;
    static Integer THREAD_MAX = 100;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
        for (int i = 0; i < THREAD_MAX; i++) {
            executorService.execute(() -> {
                threadLocal.set(new ThreadLocalTest().getList());
                System.out.println(Thread.currentThread().getName());
                // 移除对象
                threadLocal.remove(); 
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
    List getList() {
        List list = new ArrayList();
        for (int i = 0; i < MOCK_MAX; i++) {
            list.add("Version:JDK 8");
            list.add("ThreadLocal");
            list.add("Author:大冰");
            list.add("DateTime:" + LocalDateTime.now());
            list.add("Test:ThreadLocal OOM");
        }
        return list;
    }
}

面试笔试题

1.ThreadLocal 为什么是线程安全的?

答:ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。

2.ThreadLocal 如何共享数据?

答:通过 ThreadLocal 的子类 InheritableThreadLocal 可以天然的支持多线程间的信息共享。

3.以下程序打印的结果是 true 还是 false?

ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("大冰");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("大冰");
new Thread(() -> {
    System.out.println(threadLocal.get().equals(threadLocal2.get()));
}).start();

答:false。
题目分析:因为 threadLocal 使用的是 InheritableThreadLocal(共享本地线程),所以 threadLocal.get() 结果为 大冰,而 threadLocal2 使用的是 ThreadLocal,因此在新线程中 threadLocal2.get() 的结果为 null,因而它们比较的最终结果为 false。

4.解决 ThreadLocal 内存溢出的关键代码是什么?

答:关键代码为 threadLocal.remove(),使用完 ThreadLocal 之后只需要把对象移除掉,就不会发生内存溢出的问题。

5.ThreadLocal 为什么会发生内存溢出?

答:因为 ThreadLocal 的存储实际上是把当前线程作为 key,再加上要存储的值作为 value 存入到 ThreadLocalMap 中(内部实现为 Entry 数据结构),这样就会把已经存储的值和当前线程的生命周期关联起来,当线程不被回收的时候,存储的值一直不被释放,等有足够多的线程和值的时候就会造成内存溢出。

6.ThreadLocal 和 Synchonized 有什么区别?

答:ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别。Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种 “以时间换空间” 的方式;而 ThreadLocal 为每一个线程都提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 “以空间换时间” 的方式。

全部评论

相关推荐

死在JAVA的王小美:哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈,我也是,让我免了一轮,但是硬气拒绝了
点赞 评论 收藏
分享
评论
点赞
收藏
分享
正在热议
# 25届秋招总结 #
442570次浏览 4512人参与
# 春招别灰心,我们一人来一句鼓励 #
41986次浏览 533人参与
# 阿里云管培生offer #
120264次浏览 2220人参与
# 地方国企笔面经互助 #
7964次浏览 18人参与
# 同bg的你秋招战况如何? #
76743次浏览 563人参与
# 实习必须要去大厂吗? #
55775次浏览 961人参与
# 北方华创开奖 #
107437次浏览 599人参与
# 虾皮求职进展汇总 #
115687次浏览 886人参与
# 如果你有一天可以担任公司的CEO,你会做哪三件事? #
11584次浏览 287人参与
# 实习,投递多份简历没人回复怎么办 #
2454714次浏览 34857人参与
# 提前批简历挂麻了怎么办 #
149906次浏览 1977人参与
# 在找工作求抱抱 #
906025次浏览 9421人参与
# 如果公司给你放一天假,你会怎么度过? #
4757次浏览 55人参与
# 你投递的公司有几家约面了? #
33206次浏览 188人参与
# 投递实习岗位前的准备 #
1195950次浏览 18549人参与
# 机械人春招想让哪家公司来捞你? #
157635次浏览 2267人参与
# 双非本科求职如何逆袭 #
662248次浏览 7397人参与
# 发工资后,你做的第一件事是什么 #
12734次浏览 62人参与
# 工作中,努力重要还是选择重要? #
35815次浏览 384人参与
# 简历中的项目经历要怎么写? #
86920次浏览 1516人参与
# 参加完秋招的机械人,还参加春招吗? #
20133次浏览 240人参与
# 我的上岸简历长这样 #
452024次浏览 8088人参与
牛客网
牛客企业服务