首页 > 其他分享 >ThreadLocal

ThreadLocal

时间:2022-10-23 16:13:22浏览次数:44  
标签:static 对象 ThreadLocal 线程 Entry null

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()方法清理。

标签:static,对象,ThreadLocal,线程,Entry,null
From: https://www.cnblogs.com/yanghuanxi/p/16818747.html

相关文章

  • ThreadLocal原理及使用场景
    ​ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。​所谓的共享变量指的是在堆中的实例、静态属性和数组;对于共享数据的访问受Java的内存模型(JMM......
  • threadLocal
    https://juejin.cn/post/7126708538440679460每个线程持有一个threadLocalMapkey是TheadLocal,value是泛型对象publicvoidset(Tvalue){Threadt=Threa......
  • ThreadLocal、InheritThreadLocal、TransmittableThreadLocal
    一、ThreadLocal多线程是Java实现多任务的基础,​​Thread​​​对象代表一个线程,我们可以在代码中调用​​Thread.currentThread()​​获取当前线程。例如,打印日志时,可以同......
  • ThreadLocal本地局部线程demo
    ThreadLocal本地局部线程demoimportorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importjava.util.HashMap;importjava.util.Map;/***本工具只能保存一......
  • 面试必备:ThreadLocal详解
    前言大家好,我是捡田螺的小男孩。无论是工作还是面试,我们都会跟ThreadLocal打交道,今天就跟大家聊聊ThreadLocal哈~ThreadLocal是什么?为什么要使用ThreadLocal一个Thre......
  • 惊!ThreadLocal你怎么动不动就内存泄漏?
    “今天无聊带大家分析下ThreadLocal为什么会内存泄漏~前言使用ThreadLocal不当可能会导致内存泄露,是什么原因导致的内存泄漏呢?正文我们首先看一个例子,代码如下:pub......
  • ThreadLocal夺命11连问
    前言前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。我把精华浓缩了......
  • ThreadLocal
    ThreadLocal是一个数据结构,有点像HashMap,可以保存key-value键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。ThreadLocal为变量在每个线程中都创建一个......
  • PageHelper中的ThreadLocal未清空问题
    起因前几天运维发现项目中的XXL执行的时候突然报异常,看了一波异常日志,发现XXl中的普通list查询竟然跑到PageHelper中的我写的分页权限过滤器了。正常来说,我只是简单查询,应......
  • TransmittableThreadLocal和@Async优雅的记录操作日志
    此文主要讲解:如何实现操作记录如何将TransmittableThreadLocal和@Async搭配使用TransmittableThreadLocal阿里的一个开源组件,为了在使用线程池等会池化复用线程的执行......