专栏首页Spark学习技巧面试|再次讲解Threadlocal使用及其内存溢出

面试|再次讲解Threadlocal使用及其内存溢出

浪尖整理本文主要是想帮助大家完全消化面试中常见的ThreadLocal问题。希望读懂此文以后大家可以掌握(没耐心的可以直接阅读底部总结):

  1. 简单介绍原理
  2. ThreadLocal使用案例场景
  3. Threadlocal的底层原理
  4. Threadlocal内存溢出原因
  5. 解决方法

1. 简介

高并发处理起来比较麻烦,很多新手对此都会非常头疼。要知道避免并发的最简单办法就是线程封闭,也即是把对象封装到一个线程里,那么对象就只会被当前线程能看到,使得对象就算不是线程安全的也不会出现任何安全问题。Threadlocal是实现该策略的最好的方法。Threadlocal为每个线程提供了一个私有变量,然后线程访问该变量(get或者set)的时候实际上是读写的自己的局部变量从而避免了并发法问题。

2. 案例使用

首先定义一个ThreadLocal的封装工具类

package bigdata.spark.study.ThreadLocalTest;
public class Bank {    ThreadLocal<Integer> t = new ThreadLocal<Integer>(){        @Override        protected Integer initialValue() {            return 100;        }    };
    public int get(){        return t.get();    }
    public void set(){        t.set(t.get()+10);    }}

实现一个Runnable对象然后使用bank对象

package bigdata.spark.study.ThreadLocalTest;
public class Transfer implements Runnable {
    Bank bank;
    public Transfer(Bank bank) {        this.bank = bank;
    }
    @Override    public void run() {        for (int i =0 ;i < 10;i++){            bank.set();            System.out.println(Thread.currentThread()+" : " +bank.get());        }    }}

定义两个线程t1和t2,运行之后查看结果:

package bigdata.spark.study.ThreadLocalTest;
public class Test {
    public static void main(String[] args) {        Bank bank = new Bank();
        Transfer t = new Transfer(bank);                Thread t1 = new Thread(t);
        t1.start();        Thread t2 = new Thread(t);        t2.start();
        try {            t1.join();            t2.join();        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(bank.get());
    }}

查看输出结果就会发现,发现主线程,线程t1,线程t2之间相互不影响~

Thread[Thread-0,5,main] : 110Thread[Thread-0,5,main] : 120Thread[Thread-0,5,main] : 130Thread[Thread-0,5,main] : 140Thread[Thread-0,5,main] : 150Thread[Thread-0,5,main] : 160Thread[Thread-0,5,main] : 170Thread[Thread-0,5,main] : 180Thread[Thread-0,5,main] : 190Thread[Thread-0,5,main] : 200Thread[Thread-1,5,main] : 110Thread[Thread-1,5,main] : 120Thread[Thread-1,5,main] : 130Thread[Thread-1,5,main] : 140Thread[Thread-1,5,main] : 150Thread[Thread-1,5,main] : 160Thread[Thread-1,5,main] : 170Thread[Thread-1,5,main] : 180Thread[Thread-1,5,main] : 190Thread[Thread-1,5,main] : 200100

3. 底层源码

每个线程Thread内部都会有ThreadLocal.ThreadLocalMap对象,该对象是一个自定义的map,key是弱引用包装的ThreadLocal类型,value就是我们的值。

初始值

Threadlocal直接在构造的时候设置初始值。主要是要实现其initialValue方法:

new ThreadLocal<Integer>(){    @Override    protected IntegerinitialValue() {        return 100;    }};

追踪一下该方法,会发现其仅仅被一个私有方法调用了

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

解读一下setInitialValue私有方法

  1. 首先调用initialVaule方法,获取初始值。
  2. 然后获取当前线程对象的引用。
  3. 通过线程对象引用获取ThreadLocal.ThreadLocalMap对象 map。
  4. map对象不为空,就将当前threadlocal弱引用作为key,初始值为value完成初始化。
  5. Map对象为空,调用createMap方法,并完成初始化。 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

读到这可能会很好奇,为啥只是被私有方法调用,我们又无权调用该私有方法,如何实现初始化呢?也是很简单的在我们第一次调用get的时候,会调用该私有初始化方法,来真正完成初始化。

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

我们来解读一下get方法,此处就真正暴露ThreadLocal的真实面目了。

  1. 获取当前线程对象,t
  2. 通过getMap(t)方法来获取t内部的ThreadLocal.ThreadLocalMap对象。
  3. 然后判断ThreadLocalMap对象是否为空,不为空就可以通过当前Threadlocal对象获取对应的value值,存在返回,不存在跳过。
  4. 假如map为空或者当前threadlocal对象对应的value为空,那么就调用初始化方法setInitialValue初始化并返回初始值。

Set

接下来解读一下threadlocal变量的set方法。Set的方法源码如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

1. 获取当前线程对象 t

2. 通过getMap(t)方法来获取t内部的ThreadLocal.ThreadLocalMap对象 map。

3. map不为空,当前threadlocal对象作为key(弱引用),要设置的value作为value完成值的设置。

4. 假如map为空,就调用createMap方法,给当前线程创建一个ThreadlocalMap

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

remove方法

threadlocal的remove方法主要作用是删除当前threadlocal对应的键值对。

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

4. 内存泄漏

根据前面对threadlocal的整理,其实可以画出来一个结构图:

对value的引用线路有两条:

  1. threadlocalref 是ThreadLocal强引用,key是ThreadLocal变量的弱引用。由于key是弱引用,当ThreadLocalRef因不用而释放掉的时候,ThreadLocal对象就会被回收,由于是key到threadLocal对象为弱引用,一旦进行垃圾回收key就会被回收而相应位置变为null,当然value依然存在。
  2. 通过当前线程的引用可以获取当前线程对象,当前线程对象就可以获取到ThreadLocalMap,那么只要当前线程一直存在,ThreadLocalMap对象就会一直存在。

由于ThreadlocalMap存活时间和线程一样,比如我们采用的是常驻线程池,使用线程过程中没有清空ThreadLocalMap,也没有调用threadlocal的remove方法,就将线程放回线程池,虽然ThreadLocal的强引用ThreadLocalRef被清除,弱引用key在GC的时候也会被设置为null,但是对于value值还存在一条强引用链条:

currentThreadRef->currentThread->ThreadLocalMap->Entry(value)

所以value并没有释放,就造成了内存泄漏了。

那这时候你或许会问为啥ThreadLocalMap存储value的时候不采用弱引用呢?这样不就可以避免内存泄漏了么?value是弱引用是不行的,原因很简单:我们存储的对象除了ThreadLocalMap的Value就没有其他的引用了,value一但是对象的弱引用,GC的时候被回收,对象就无法访问了,这显然不是我们想要的。

5. 避免内存泄漏

为避免内存泄漏最好在使用完ThreadLocal之后调用其remove方法,将数据清除掉。

当然,对于Java8 ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏

get方法会间接调用expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收

本文分享自微信公众号 - Spark学习技巧(bigdatatip),作者:浪尖

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 美团面试题:JVM堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    Spark学习技巧
  • Kafka源码系列之Broker的IO服务及业务处理

    Kafka源码系列之Broker的IO服务及业务处理 一,kafka角色 Kafka源码系列主要是以kafka 0.8.2.2源码为例。以看spark等源码的经...

    Spark学习技巧
  • 深入理解 hashcode 和 hash 算法

    作为一个有抱负的 Java 程序员,在经过长期的CRUD 和 HTML 填空之后必须有所思考,因为好奇心是驱动人类进步的动力之一,我们好奇,比如我们常用的 Ha...

    Spark学习技巧
  • [javaSE] 看博客学习多线程的创建方式和优劣比较和PHP多线程

    Runnable是一个接口,定义一个类MyRunnable实现Runnable接口,实现run()方法,

    陶士涵
  • 史上最全 Java 中各种锁的介绍

    在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。 ...

    java金融
  • 美团面试题:JVM堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    Spark学习技巧
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    芋道源码
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    乔戈里
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    良月柒
  • java多线程|创建线程的各种方式

    本网站记录了最全的各种JavaDEMO ,保证下载,复制就是可用的,包括基础的, 集合的, spring的, Mybatis的等等各种,助力你从菜鸟到大牛,记得...

    微笑的小小刀

扫码关注云+社区

领取腾讯云代金券