ThreadLocal

#简介

ThreadLocal,线程局部变量。ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

#内部结构

structure-of-threadlocal.png

由结构图可知,ThreadLocal的一些核心机制:

  • 每个Thread线程内部都有一个Map;
  • Map里面存储线程本地对象(key)和线程的变量副本(value);
  • Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置线程变量值。

因此对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

Thread线程内部的Map类如下:

public class Thread implements Runnable {
    ......
    // 与此线程有关的ThreadLocal值,由ThreadLocal类维护。
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ......
}

ThreadLocal类提供以下方法:

get()方法:用于获取当前线程的副本变量值。

  • 获取当前线程的ThreadLocalMap对象;
  • 从map中获取存储的K-V Entry节点;
  • 从Entry节点中获取存储的Value副本值返回;
  • map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
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();
}

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

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

set()方法:用于保存当前线程的副本变量值。

  • 获取当前线程的的ThreadLocalMap对象;
  • map非空时重新将ThreadLocal和新的value副本放入到map中;
  • map为空时对成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本值放入map中。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

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

remove()方法:移除当前线程的副本变量值。

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

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

#关于内存泄漏

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

在ThreadLocalMap中,使用Entry来保存K-V结构数据。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

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

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

ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收时key会被清理掉而value不会被清理掉。这样,ThreadLocalMap中就会出现key为null的Entry。加入不做任何措施,value永远无法被GC回收,这种时候可能造成内存泄漏。

因此为了避免内存泄漏问题,在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

#应用场景

ThreadLocal处理SimpleDateFormat线程不安全问题。

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * ThreadLocal处理SimpleDateFormat线程不安全问题
 */
public class ThreadLocalDemo implements Runnable {
    private static final ThreadLocal<SimpleDateFormat> FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(demo, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name = " + Thread.currentThread().getName() + "; Default Formatter = " + FORMATTER.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        FORMATTER.set(new SimpleDateFormat());
        System.out.println("Thread Name = " + Thread.currentThread().getName() + "; Formatter = " + FORMATTER.get().toPattern());
    }
}

执行结果:

application-of-threadlocal.png

由输出可知,Thread0已经改变了FORMATTER的值,但Thread1的初始化值仍然与默认初始化值相同,其他线程也一样。