面试题:ThreadLocal 的内存泄漏问题

一、ThreadLocal 简介

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
  
使用ThreadLocal在各自线程内部保存变量并使用的例子:

package com.example.demo.questions;
public class ThreadLocalDemo {
    static ThreadLocal<String> localVar = new ThreadLocal<>();
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

输出结果:

thread2 :localVar2
thread1 :localVar1
after remove : null
after remove : null

这里可以看到在线程t1和线程t2内部,各自设置了一个变量localVar1和localVar2,虽然ThreadLocal是一个全局变量,但是两个线程在使用和销毁的时候并没有发生线程安全问题。这是为什么呢?

二、ThreadLocal实现原理

其实副本变量还是线程内部自己的处理结果,Thread类中有一个ThreadLocal.ThreadLocalMap对象

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

ThreadLocal只是做了一个外部统筹的工作。我们可以看一下ThreadLocal的set(value)方法的源码:

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

可以看到其实内部会先获取到当前线程,然后通过getMap(t)方法获取到当前线程的ThreadLocalMap对象,然后再将参数value保存到ThreadLocalMap对象中key为当前ThreadLoca对象(不是当前线程)

ThreadLocal的get()方法呢,很显然也是先获取当前线程对象,再获取它的ThreadLocalMap对象,然后从map里面得到对应的value:

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

这样就明白了ThreadLocal保存副本变量的机制其实就是将变量存入每个线程自己内部的ThreadLocalMap对象中。

三、内存泄漏问题

此时有一个问题,由于ThreadLocalMap对象中的key是ThreadLocal对象,value使我们的存放的变量。如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要调用ThreadLocal的remove()方法将变量移除掉。为了减少人为忘记remove的问题,ThreadLocalMap在设计的时候,将每个节点中保存的key值封装了一层弱引用,这样在每次gc的时候都会回收掉这个ThreadLocal弱引用对象

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

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

这样可以减少内存溢出的问题,但是不能完全避免,主要还是代码执行完成之后手动调用remove()方法

四、InheritableThreadLocal类

1. InheritableThreadLocal简介

使用ThreadLocal时,主线程和子线程以及各个子线程之间的副本变量都是不能互相访问的。但是如果想要在子线程中访问主线程中的副本变量,可以通过InheritableThreadLocal这个类来实现:

package com.example.demo.questions;
public class InheritableThreadLocalDemo {
    public static InheritableThreadLocal<String> localVar = new InheritableThreadLocal<>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
    }

    public static void main(String[] args) throws InterruptedException {
        localVar.set("mainVar");
        print("mainThread");

        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                print("thread1");
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
            }
        });

        t1.start();
        Thread.sleep(1000);
        print("mainThread");

    }
}

输出结果:

mainThread :mainVar
thread1 :mainVar
thread1 :localVar1
mainThread :mainVar

可以看到子线程是可以访问到主线程先设置的mainVar变量的,但是主线程并不能访问子线程的副本变量

2. 实现原理

(1)首先,Thread类中还有一个inheritableThreadLocals变量就是用来保存父线程中传递过来的变量的:

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

(2)在我们在主线程调用InheritableThreadLocal的set()方法时,如果获取ThreadLocalMap时对象为空的话,它内部的createMap()方法其实是将创建的ThreadLocalMap赋值给了线程的inheritableThreadLocals对象,:

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

而ThreadLocal内部的createMap()方法则是将ThreadLocalMap对象赋值给了线程threadLocals对象:

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

(3)然后子线程在初始化的时候,会先判断父线程中的inheritableThreadLocals对象是否为空。如果不为空,则把父线程的inheritableThreadLocals对象拷贝给自己

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
    ......
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        	ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ......
}

(4)然后我们在子线程中调用InheritableThreadLocal对象的get()方法时,其实是从当前子线程的inheritableThreadLocals对象中获取的,也就是从父线程拷贝过来的对象:

ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

这样我们就通过InheritableThreadLocal类实现了子线程中访问主线程副本变量的效果,其实就是一个拷贝变量的过程。


THE END.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值