java基础之ThreadLocal

1、简单概述

在研究多线程的时候,处理多线程共享变量,使用锁来解决之外;但是有时候并非是想只通过这样一种方式来进行解决,而是想要每个线程都使用唯一的一个变量来进行使用,比如说解决数据库的一致性问题,通过同一个connection来进行解决。

那么就是通过ThreadLocal的方式来进行解决,并非是通过线程同步的方式,而是使用线程隔离的方式来进行解决的。

参考博客:https://blog.csdn.net/weixin_44050144/article/details/113061884

从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

从这段话中提取出来关键的信息:每个线程中的变量是全局变量的一份拷贝,而在每个线程中单独操作时不会影响到其他线程中该变量的值,这样达到的效果就是线程之间相互隔离了。而且从最后一句话中可以得知,可以在同一个线程中的多个函数调用之间达到变量传递的效果。

也就是说我们可以利用这个技巧可以解决实际上的问题,比如说,方法扩展上,如果方法需要多加一个参数,那么我们就可以利用线程隔离的手段,在线程上绑定上一个变量,而不是需要将方法立即给修改掉,从而达到想要的效果。

总结一下ThreadLocal的使用场景介绍:

总结:
1. 线程并发: 在多线程并发的场景下;
2. 传递数据: 我们可以通过ThreadLocal在同一线程不同组件、函数中传递公共变量
3. 线程隔离: 每个线程中的变量都是独立的,不会互相影响。也就是说实现了多线程之间对同一个共享数据的隔离;

但是需要注意区别,在选择使用的时候注意二者的使用场景是有着很大的不同的。一个是在线程同步条件下,而threadlocal不需要保证线程同步,它强调的是线程隔离的条件下来使用变量,所以在这种情况下,不算叫做共享变量。

在使用之前,我们先来认识几个ThreadLocal的常用方法

方法声明 描述
ThreadLocal() 创建ThreadLocal对象
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

我们来看下面这个案例 , 感受一下ThreadLocal 线程隔离的特点:

public class MyDemo {
    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。

因为什么?使用使用的是同一个对象来进行操作的。对象最终展示的是能够及时看到的对象的属性值。

那么使用ThreadLocal来观察:

public class MyDemo1 {

    private static ThreadLocal<String> tl = new ThreadLocal<>();

    private String content;

    private String getContent() {
        return tl.get();
    }

    private void setContent(String content) {
         tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

将结果打印到控制台,查看输出信息,可以看到线程之间实现了隔离。从这里可以观察出来什么内容?及时是使用对象来进行操作,但是对象操作的方法中是通过threadlocal来进行操作的。

转账的案例先不看了,具体的来看下源代码。

2、ThreadLocal结构

首先看下ThreadLocal的结构:

早期的设计结构图如下所示:

java基础之ThreadLocal

对于一个ThreadLocal来说,里面维护了一个ThreadLocalMap,map集合中维护了一个Entry,key是每个thread,value就是每个线程的绑定的值。

这种方式比较符合人们的理解方式。但是这种方式在之后就给废弃了。但是没有对比就没有伤害,看看最新的方式

jdk8中的设计方式:

java基础之ThreadLocal

可以看到每个线程维护了一个ThreadLocalMap,而每个ThreadLocalMap中存在着一个Entry,其中key是threadlocal,value就是每个线程保存的孩子。

可以看到这里和上面的区别就在于:将threadlocal和thread调换了一个位置。

所以这里简单介绍下区别:

1、对于多线程环境下来说,每个线程对象都有一个threadlocalmap,这个是肯定的,但是每个threadlocal是否都有着对应的值呢?
也就是说是否存在着Entry?也就是说在真实场景下threadlocalmap是存在的,但是并不一定存在着entry。也就是说threadlocalmap的数量是少于thread的,因为有的线程可能并没有使用到threadlocalmap,换句话说为null
    
2、thread销毁之后,threadlocalmap也会随之消失,因为threadlocalmap是依赖于thread而存在的。既然thread消失了,那么threadlocalmap也就被垃圾回收了    

在看过源码之后,会发现里面有这样的一个特点,里面的entry中的key继承了弱引用,那么聊一下弱引用和内存泄漏的问题。

内存泄漏:这个在底层语言c和c++中是很好理解的,也就是说程序员手动的来分配内存,在使用完毕之后,需要手动的将内存释放掉。保存安全性。如果没有进行释放,那么程序的其他地方也就无法来访问这块内存空间。如果出现了大量的这种情况,总会导致程序越来越卡,导致程序最终无法运行。

内存溢出:这种也是比较常见的场景,比如说递归方法中最常用的,也就是调用方法的时候,导致了OS分配给程序的可用空间不足以满足程序的运行了,最终导致了这种现象的发生。

弱引用:因为java程序没有了C语言和c++的手动释放的习惯,而是采用了垃圾回收算法来将程序员在请求操作系统来分配内存后,使用完成之后没有手动释放的情况,采用了垃圾回收算法来将这里JVM认为是“垃圾”的内存空间来进行清楚掉。java中的引用我们可以看成是指针,java中分为了强软弱虚的概念。这里将一下弱引用的概念,其实其他的也是差不多的,弱引用就是只要是JVM检测到了,那么就可以进行回收掉。

假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

此时ThreadLocal的内存图(实线表示强引用)如下:

java基础之ThreadLocal

假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

但是因为threadLocalMap的Entry中的key强引用了threadLocal,造成threadLocal无法被回收。

在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?

此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:

java基础之ThreadLocal

同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

从上面可以看到,currentThread仍然在运行的情况下,key是强引用的时候,key泄漏;如果key是弱引用的情况下,value存在着内存泄漏。

那么根本原因是什么?首先假想一下,如果currentthread没有了指向的时候,那么也就说明了map没有了指向,此时map也就没有了外部的指向是否是可行的?

线程运行结束之后,每个线程都会被GC给回收掉,map也会回收掉,最终的Entry也会被回收掉。

但是如果说当前线程依然存在的情况下,如何来进行处理?首先线程依然存在的时候,当使用完了局部变量之后,手动的将Entry给移除掉

对象的属性依赖于当前的对象而存在,所以属性的依赖是依赖于对象的。但是我们应当适量的避免这种依赖性强的,低内聚。

也就是说尽量减少依赖性,但是无法完全避免这种依赖。

3、源码分析

接下来看看源码:

先看下get方法:

  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

总结下流程:

 A. 首先获取当前线程,并根据当前线程获取一个Map

? B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

? C. 如果Map为空,则给该线程创建 Map,并设置初始值

再看下set方法:

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

具体分析下执行流程:

 A. 首先获取当前线程, 根据当前线程获取一个Map

 B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D

 C. 如果e不为null,则返回e.value,否则转到D

 D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

再看下remove方法:

 /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }


        /**将key进行移除掉。那么这里对value的处理?
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    // 可以看到这里已经将null也来进行重置
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

执行流程:

 A. 首先获取当前线程,并根据当前线程获取一个Map

? B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

再来看下initialValue方法,这个方法一般来说,根据自己的场景来进行重写。

/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

此方法的作用是 返回该线程局部变量的初始值。

(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

(2)这个方法缺省实现直接返回一个null。

(3)如果想要一个除null之外的初始值,可以重写此方法。

4、ThreadLocalMap

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

java基础之ThreadLocal

成员变量:

    /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0

存储的基本结构:

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

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

另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

ThreadLocal中的set方法:

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用了ThreadLocalMap的set方法
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        	//调用了ThreadLocalMap的构造方法
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :

A. 首先获取当前线程,并根据当前线程获取一个Map

B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

(这里调用了ThreadLocalMap的set方法)

C. 如果Map为空,则给该线程创建 Map,并设置初始值

(这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析这两个方法。

 /*
  * firstKey : 本ThreadLocal实例(this)
  * firstValue : 要保存的线程本地变量
  */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引(重点代码)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //设置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //设置阈值
        setThreshold(INITIAL_CAPACITY);
    }

5、实际应用

这里如果用到了到时候再回来进行补充。通过一个实际案例来结合分析一波:

/**
 * 在多线程条件下,直接将PERSON和每个线程来进行绑定。通过PERSON来操作,实现每个线程之间的隔离性,可以看到利用set/get方法来达到
 * 响应的操作目的,而且辖区内城之间的值互相不干扰
 */
@Slf4j
public class ThreadLocalTest2 {

    private static final ThreadLocal<Person> THREAD_LOCAL = new ThreadLocal<>();

    private static final Person PERSON = new Person();

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                // 对于THREAD_LOCAL来说,二者的set和get可能不是处于同一个地方,但是这里来进行模拟的时候是这样子来进行操作。
                THREAD_LOCAL.set(PERSON);
                Person person = THREAD_LOCAL.get();
                // 这样子来写,直接报错。
                // 可以看到这里并不去提供修改绑定的状态来,只是提供了set/get方式来进行使用。使用这个线程绑定,更倾向于利用的是里面的方法,而不是属性
                person.setId((int) Thread.currentThread().getId());
                person.setUsername(""+((int) Thread.currentThread().getId()-11));
                // 可以看到对象是同一个地址值。但是变量的值已经都给改变了
//                log.info("person对象的hashcode值是:{}",THREAD_LOCAL.get());
                // 尽管地址值是同一个,但是里面的属性已经发生了改变。
                log.info("person对象的id值是:{},对应的username值是:{}",THREAD_LOCAL.get().getId(),THREAD_LOCAL.get().getUsername());
                THREAD_LOCAL.remove();
            }).start();
        }
    }
}

通过上面的案例可以看到如果我们这么来进行操作,发现对象的地址都是一样的。但是对于对象的属性来说,每个线程中的都不一样。

再次回到第一回讲到的官方API中讲述的:

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

java基础之ThreadLocal

上一篇:Android开发——自定义view之文字绘制


下一篇:go_["a","a","b","b","c","c","c"]非数字数组json.Unmarshal()方法不支持