Skip to content

进阶之路-揭开ThreadLocal神秘面纱

阅读本文主要可以解决以下困惑:

  1. 什么是ThreadLocal,隔离线程的本地变量
  2. ThreadLocal的数据结构是怎么样的,为什么能实现线程隔离
  3. ThreadLocal的get和set方法
  4. ThreadLocal如何实现的线程安全?结合同步锁机制,空间换取时间
  5. ThreadLocal为什么会出现内存泄漏
  6. ThreadLocal在Handler中的应用?如何保证Thread和Looper的一一对应关系的

ThreadLocal是什么

ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。 根据这一特点ThreadLocal可以隔离线程

ThreadLocal的数据结构

如何理解ThreadLocal的结构 ThreadLocal的数据结构类似于 HashMap<Thread , ThreadLocalMap<ThreadLocal , T>>首先我们根据线程Thread作为Key值获取到,每一个线程中的 ThreadLocalMap,而ThreadLocalMap又是一个Map结构,其中ThreadLocal实例作为Key值,存储在该ThreadLocal中的值作为ValueThreadLocalMap在ThreadLocal中get()方法和set()方法都有可能创建该线程的ThreadLocalMap(最终他们都调用了createMap()方法

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

ThreadLocalMap,这个类没有实现map的接口,就是一个普通的java类,但是实现的类就类似于map的功能,数据用Entry存储,Entry继承于WeakReference,用一个键值对来存储**,键就是ThreadLocal的引用**。每一个线程都有一个ThreadLocalMap的对象,每一个新的线程Thread都会实例化一个ThreadLocalMap并赋予值给成员变量Thread.threadLocals。

java
public class ThreadLocalTest {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        new Thread(runnable,"线程1").start();
        new Thread(runnable,"线程2").start();
    }
    public static class MyRunnable implements Runnable{
        ThreadLocal<String> threadLocal1= ThreadLocal.withInitial(() -> "null");
        ThreadLocal threadLocal2= ThreadLocal.withInitial(() -> "null");
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            threadLocal1.set(name+"的threadLocal1");
            threadLocal2.set(name+"的threadLocal2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(name + ": "+ threadLocal1.get());
            System.out.println(name + ": "+ threadLocal2.get());
        }
    }
}

线程2: 线程2的threadLocal1
线程1: 线程1的threadLocal1
线程2: 线程2的threadLocal2
线程1: 线程1的threadLocal2

源码分析

ThreadLocal#get()

java
public T get() {
    //1.获取到当前的线程
    Thread t = Thread.currentThread();
    //2.通过当前线程获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //3.通过当前的ThreadLocal对象
        //获取到Entry对象(其中封装着value)
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocal#set()

java
public void set(T value) {
    //1.获取到当前线程
    Thread t = Thread.currentThread();
    //2.根据线程获取到ThreadLocalMap
	//由此可知Thread作为Key,而ThreadLocalMap作为Value
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocal如何做到线程安全

每个线程拥有自己独立的**ThreadLocals**变量(指向ThreadLocalMap对象 ) 每当线程 访问 **ThreadLocals**变量时,访问的都是各自线程自己的**ThreadLocalMap**变量(键 - 值) **ThreadLocalMap**变量的键 key = 唯一 = 当前ThreadLocal实例

ThreadLocal与同步机制的区别

image.png

为什么ThreadLocal的键是弱引用

image.png 如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key ,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。

ThreadLocal的内存泄漏

重点来了,如果一种情况下我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap(因为它是Thread类 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。 解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。 所以 如同 lock 的操作 最后要执行解锁操作一样,ThreadLocal使用完毕一定记得执行remove 方法,清除当前线程的数值。 如果不remove 当前线程对应的VALUE ,就会一直存在这个值。 使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

ThreadLoacal的使用实例

首先讲解以下Handler中四兄弟的对应关系 Looper和MeassgeQueue一一对应,因为消息队列是Looper创建的。这个很好理解 Thread和Looper也是一一对应,那这种对应关系是如何建立的呢? 例如我们在一个子线程中要使用Handler,我们首先会去创建一个Looper,使用Looper.prepare()初始化当前线程的Looper,而在Looper的内部又会创建消息队列。

java
new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();			//初始化Looper
        new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
        Looper.loop();//开启消息循环
    }
})

可以看到源码中prepare方法的实现就是,将Looper保存在了一个ThreadLocal中,因此每一个线程在创建Looper的时候,这种Thread和ThreadLocal的一一对应关系就建立了。而Looper又存放在当前Thread对应的ThreadLooper中,所以Thread与Looper的一一对应关系是借助着ThreadLocal建立的

java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
  ···
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

再看一下Looper和MessageQueue的一一对应关系的建立,深入以上代码的new Looper(quitAllowed);可以看到就是在私有的构造函数中创建了一个MessageQueue对象。

java
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}