ThreadLocal

java / 2022-10-23
1 1,307

ThreadLocal初衷是在线程并发时解决变量共享问题,但由于过度设计,比如弱引用和哈希碰撞,导致理解难度大、使用成本高,反而成为了故障高发点。容易出现内存泄漏、脏数据、共享对象更新等问题。

01引用类型

强引用:Object obj = new Object();这就属于强引用。只要对象有强引用,并且GC Roots可达,那么在内存回收时,即使内存耗尽也不会回收。
软引用:在发生OOM时,会回收没有被引用的对象;没有OOM,即使对象引用为null也不会回收。
弱引用:没有被引用的对象,在下次YGC时会被回收。obj = null,obj对象在下次YGC时就会被回收。
虚引用:为一个对象设置虚引用唯一的目的是希望在这个对象被回收时收到一个系统通知,虚引用必须与引用队列联合使用。
WeakReference这种特性也用在了ThreadLocal上。JDK设计的初衷是在ThreadLocal对象消失后,线程对象在持有这个ThreadLocal对象是没有意义的,应该进行回收,从而避免内存泄漏。这种设计出发点是好的,但是实际业务中却并非如此,弱引用的设计反而增加了对ThreadLocal和Thread体系的理解难度。
总结:
强引用:不回收
软引用:内存不足时回收
弱引用:GC时回收
虚引用:一般用于跟踪对象是否可达,不可达则回收
02ThreadLocal价值
同一线程变量共享。
从真人 cs游戏说起。游戏开始时,每人都能领到一把枪,枪把上有子弹数、生命数和杀敌数。假设每一个人都是一个线程,那这三个初始值应该写在哪里呢?如果写死,后面临时要改呢?如果共享,线程间的并发修改又会导致数据不准确。能不能构造一个对象,将这个变量设置为共享变量,统一设置初始值,但是每个线程对这个值的修改都是独立的。这个对象就是ThreadLocal。

public class CsGameByThreadLocal {
    private static final Integer BULLET_NUMBER = 500;
    private static final Integer KILLED_ENEMIES = 0;
    private static final Integer LIFE_VALUE = 10;
    private static final Integer TOTAL_PLAYER = 5;

    // 随机每个对象的不同数值
    private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();

    // 初始化子弹数
    private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = ThreadLocal.withInitial(() -> BULLET_NUMBER);

    // 初始化杀敌数
    private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = ThreadLocal.withInitial(() -> KILLED_ENEMIES);

    // 初始化生命数
    private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = ThreadLocal.withInitial(() -> LIFE_VALUE);

    // 定义每一位玩家
    private static class Player extends Thread {
        @Override
        public void run() {
            int bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);
            int killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYER / 2);
            int lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE);
            System.out.println(getName() + ", BULLET_NUMBER is " + bullets);
            System.out.println(getName() + ", KILLED_ENEMIES is " + killEnemies);
            System.out.println(getName() + ", LIFE_VALUE is " + lifeValue);

            BULLET_NUMBER_THREADLOCAL.remove();
            KILLED_ENEMIES_THREADLOCAL.remove();
            LIFE_VALUE_THREADLOCAL.remove();
        }
    }


    public static void main(String[] args) {
        for (Integer i = 0; i < TOTAL_PLAYER; i++) {
            new Player().start();
        }
    }

}

此实例中,每个线程在执行ThreadLocal.get的时候会执行initialValue方法。

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,如果map==null,则执行setInitialValue。如果map已经创建,就表示Thread类的threadLocals属性已经初始化;如果e==null,依然会执行到setInitialValue。setInitialValue方法源码如下:
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;
}
注:应该尽量避免在多线程中使用Random类来获取随机数,虽然共享该实例是线程安全的,但会因竞争同一seed而导致性能下降。
注:ThreadLocal无法解决共享对象的更新问题。
ThreadLocal和Thread的类关系图,了解其主要方法。
yuque_diagram.jpg
ThreadLocal有个静态内部类ThreadLocalMap,它还有一个静态内部类叫Entry,在Thread中的ThreadLocalMap属性赋值是在ThreadLocal类中的createMap中进行的。ThreadLocal与ThreadLocalMap有三组对应的方法:get()、set()和remove(),在ThreadLocal中对他们只做校验和判断,最终的实现会落在ThreadLocalMap中。Entry继承自WeakReference,没有方法,只有一个value成员变量,它的key是ThreadLocal对象。再从栈与堆的内存角度看看两者的关系。
yuque_diagram1.jpg
上图中的简要关系:

  • 1个Thread有且仅有1个ThreadLocalMap对象;
  • 1个Entry对象的Key弱引用指向1哥ThreadLocal对象;
  • 1个ThreadLocalMap对象存储多个Entry对象;
  • 1个ThreadLocal对象可以被多个线程所共享;
  • ThreadLocal对象不持有Value,Value由线程的Entry对象持有

Entry的源码如下:

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

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

所有Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收,由于Entry中的ThreadLocal是弱引用,只要ThreadLocal对象引用被置为null,即使线程正在执行,Entry的key也会在下一次YGC被回收。而在ThreadLocal使用set和get方法时,又会自动将那些key==null的value置为null,使value能够被垃圾回收,但是现实很残酷,ThreadLocal源码中的注释写到:ThreadLocal对象通常作为静态变量使用,那么其生命周期至少不会随着线程结束而结束。

线程使用ThreadLocal有三个重要方法:

  • set():如果没有set操作的ThreadLocal,容易引起脏数据问题
  • get():始终没有get操作的ThreadLocal对象是没有意义的
  • remove():如果没有remove操作,容易引起内存泄漏

ThreadLocal用于同一个线程内,跨类,跨方法传递数据。如果没有ThreadLocal,线程内传递信息必须靠返回值和参数,无形之中又会耦合。

/**
     * Initializes a Thread.
     *
     * @param g the Thread group
     * @param target the object whose run() method gets called
     * @param name the name of the new Thread
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.
     * @param acc the AccessControlContext to inherit, or
     *            AccessController.getContext() if null
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

inheritThreadLocals为true时,可以把当前线程的变量继续传递给它的子线程。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
parent是它的父线程,createInheritedMap就是调用ThreadLocalMap的私有构造方法来产生一个实例对象,把父线程不为null的线程变量全部拷贝过来:

/**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

某宝很多场景就是通过ThreadLocal来传递上下文的,比如用ThreadLocal来存储监控系统的某个标记位,且命名为traceId,某次请求下traceId都是一致的,以获得可以统一解析的日志文件。但在实际开发中,发现子线程中的traceId都是null,跟主线程并不一致,所以就需要inheritThreadLocals来解决父子线程间共享线程变量的问题。

public class RequestProcessTrace {
    private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_THREADLOCAL = new InheritableThreadLocal<>();
    
    public static FullLinkContext getContext(){
        FullLinkContext fullLinkContext = FULL_LINK_THREADLOCAL.get();
        if (fullLinkContext == null){
            FULL_LINK_THREADLOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_THREADLOCAL.get();
        }
        return fullLinkContext;
    }
    
    
    public static class FullLinkContext {
        private String traceId;
        public String getTraceId(){
            if (StringUtils.isEmpty(traceId)){
                
            }
            return traceId;
        }
        
        public void setTraceId(String traceId){
            this.traceId = traceId;
        }
    }
}

使用ThreadLocal和InheritableThreadLocal透传上下文时,需要注意线程间的切换、异常传输时的处理。
最后,SimpleDateFormat是线程不安全类,定义为static对象,会有数据同步风险。通过源码看出,内部有一个Calendar对象,在日期转字符串或字符串转日期的过程中,多线程共享时有非常高的几率出错,推荐的方式之一就是使用ThreadLocal,让每个线程单独拥有这个对象。示例代码如下:

private static final ThreadLocal<DateFormat> DATE_FORMAT_THREADLOCAL = new 
	ThreadLocal<DateFormat>(){
	@Override
	protected DateFormat initialValue(){
		return new SimpleDateFormat("yyyy-MM-dd");
	}
}

03ThreadLocal的副作用

为了使线程安全的共享变量,JDK提供了ThreadLocal。但是ThreadLocal有一定的副作用,会产生脏数据和内存泄漏的问题。这两个问题主要是在线程池中使用ThreadLocal引发的,因为线程池有复用和内存常驻两个特点。

1、脏数据

线程复用会产生脏数据。由于线程池会重用 Thread 对象,那么与Thread绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程run0 方法体中不显式的调用remove() 清理与线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用set()设置初始值,就可能get()到重用的线程信息,包括 ThreadLocal 所关联的线程对象的valve 值。

2、内存泄漏

在源码注释中提示使用static关键字来修饰ThreadLocal,在此场景下,就寄希望于ThreadLocal对象失去引用后,触发弱引用来回收Entry的value就不现实了。
以上两个问题的解决办法很简单,就是在每次用完ThreadLocal后,及时调用remove()方法清理。