ThreadLocal使用详解

一、ThreadLocal的使用场景

线程局部变量。为线程中一个本地变量的副本提供索引,ThreadLocal可以用来维护与当前线程相关的一些上下文,不需要通过每个方法调用将其作为参数传递。

使用threadLocal一定要注意内存泄漏,否则还是建议定义context类,保存每个线程自身上下文

二、ThreadLocal分析

API
四个主要方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • 每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

  • Thread内部存在一个ThreadLocalMap,key为ThreadLocal自身,值为线程局部变量

  • 初始时,在Thread里面,threadLocals(ThreadLoaclMap的实例)为空,当通过ThreadLocal变量调用get()方法时,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为key,使用默认初始化的value作为值,存到threadLocals。

注意: 这里初始化的value默认是null,如果get之前没有调用set方法,会报空指针异常,我们创建ThreadLocal变量是可以重写initialValue()方法。

private static ThreadLocal<String> strThreadLocal = new ThreadLocal<String>() {
        public String initialValue() {
            return "default";
        }
    };

三、ThreadLocal内存泄漏

tips:
四种类型的引用

  1. 强引用
    new 对象
    Object obj = new Object();
    正常垃圾回收

  2. 软引用
    内存空间不够,垃圾回收时会回收其内存
    Object obj = new Object();
    SoftReference sf = new SoftReference(obj);
    obj = null;
    sf.get();//有时候会返回null

  3. 弱引用
    弱引用的对象,可以存活到下一次垃圾回收
    Object obj = new Object();
    WeakReference wf = new WeakReference(obj);
    obj = null;
    wf.get();//有时候会返回null
    wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

  4. 虚应用
    不影响对象的生命周期。
    虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
    如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

内存泄漏: 本来应该被回收的对象因为某种原因无法被回收

为什么使用弱引用 ?

注意上图两条引用链,ThreadLocalMap的生命周期跟Thread一样长

  • key使用强引用: 引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
  • key使用弱引用: 引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏

为什么会内存泄漏?

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏

Threadlocal有以下优化:

  1. map中key使用弱引用,在一次gc后,可以会被自动回收,变成null
  2. 每次调用set,get,remove方法会自动清除value对应为空的对象

即使有这样的优化也会存在问题:

  • 线程运行周期长。在多线程不断set放入一个大对象的时候,其中一个线程运行周期比较长,在这期间没有发生gc,ThreadLocalMap中的key就不会及时回收。假设使用线程池操作threadlocal对象,由于核心线程不会被回收,线程一直持有threadlocalMap对象,可能会造成内存泄漏
  • 程序运行异常中断,set之后再也不会调用threadLocal相关方法,都会造成内存泄漏。

为了避免内存泄漏,建议使用remove方法:

//伪代码
try {
    threadLocal.set()
    业务逻辑...
    threadLocal.get()
} catch(){
} finally{
    threadLocal.remove()
}

简单代码示例:

public class Test {
    ThreadLocal<String> stringLocal = new ThreadLocal<String>() {
        public String initialValue() {
            return "default";
        }
    };
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {
        public Long initialValue() {
            return 0L;
        }
    };

    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public void get() {
        System.out.println("longLocal: " + longLocal.get());
        System.out.println("stringLocal: " + stringLocal.get());
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
        System.out.println("获取默认值");
        test.get();
        System.out.println("----------");
        System.out.println("主线程:");
        test.set();
        test.get();
        System.out.println("----------");
        //开启一个线程
        Thread t1 = new Thread() {
            public void run() {
                System.out.println("t1线程:");
                test.set();
                test.get();
            }
        };

        t1.start();
        t1.join();
        System.out.println("-----------");
        System.out.println("验证主线程:");
        test.get();
        //在主线程和t1线程中,各自创建了一个变量副本,stringLocal ,longLocal不一样
    }
}


结果:  
获取默认值
longLocal: 0
stringLocal: default
----------
主线程:
longLocal: 1
stringLocal: main
----------
t1线程:
longLocal: 11
stringLocal: Thread-0
-----------
验证主线程:
longLocal: 1
stringLocal: main

你可能感兴趣的:(并发编程,Java,ThreadLocal)