Java multi-threading - Detailed ThreadLocal depth analysis, implementation principles, methods, and memory usage scenarios leak prevention

1 Introduction

From the name of the look, ThreadLocal is a combination of thread and local, that is, a thread there is a local copy of the variable's

ThreadLocal provides a local copy of the thread, which means that each thread will have its own copy of the independent variables

A method simple and capable, type of information and a method are listed below

 

 

 

 

 

 

 

 

2. Example

Test class defined in a ThreadLocal variable to hold a String data type, creating two threads, each set value, the value is read, read again after removing

class T21 {
    //定义ThreadLocal变量
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            //thread1中设置值
            threadLocal.set("this is thread1's local");
            //获取值
            System.out.println(Thread.currentThread().getName() + ": threadLocal value:" + threadLocal.get());
            //移除值
            threadLocal.remove();
            //再次获取
            System.out.println(Thread.currentThread().getName() + ": after remove threadLocal value:" + threadLocal.get());
        }, "thread1");
        Thread thread2 = new Thread(() -> {
            //thread2中设置值
            threadLocal.set("this is thread2's local");
            //获取值
            System.out.println(Thread.currentThread().getName() + ": threadLocal value:" + threadLocal.get());
            //移除值
            threadLocal.remove();
            //再次获取
            System.out.println(Thread.currentThread().getName() + ": after remove threadLocal value:" + threadLocal.get());
        }, "thread2");
        thread1.start();
        thread2.start();
    }
}

result:

Can see from the results, each thread can have its own unique piece of data does not affect each other, and then remove, the data is cleared

It can also be seen from the above example out of a case: 

If two threads of a variable operation between each other is not affected, in other words, it is clear that this is not a problem to solve a number of concurrent shared variables, such as multi-threaded collaboration 

Because ThreadLocal design philosophy is shared becomes private, we have been private, but also talk about what to share? 

Before such message queue, exemplary producers and consumers in 

  final LinkedList<Message> messageQueue = new LinkedList<>(); 

If this LinkedList is the ThreadLocal, producers use one, consumers use one, what it also collaborate? 

However, the private share change, as concurrent serial change, perhaps suited to address some of the scenes of thread safety issues, because it looks as if there is no shared variables, do not share that is safe, but he is not to solve thread safety issues exist

3. To achieve analysis

Thread threadLocals a variable, the type ThreadLocal.ThreadLocalMap

 

The ThreadLocalMap is static inner class ThreadLocal, he is a custom designed to save the thread local variables of the hash map

All methods of operation are private, that is not exposed to any outside method of operation, which is only used in a ThreadLocal 

Here we are not in-depth, simply understood as a hash map, used to store key-value pairs

 

 

 

 

 

In other words, there is a Thread "hashMap" can be used to store key-value pairs

 

 

 

 

 

 

 

 

In this process, we accept parameter value of type T 

First get the current thread, and then call getMap (t) 

This method is very simple, it is returned directly inside the Thread that "hashMap" (threadLocals is the default access permissions)

 

 

 

 

 

 

Continue back to the set method, if the map is not empty, then as this is key, value is the value that is variable as a key ThreadLocal 

If the map is empty, then carry out this thread to create a map, and a first set of values ​​into, key still this ThreadLocal variable

 

 

 

 

 

In short : a call to the set method of ThreadLocal, will be: with this type of ThreadLocal variable key, for the value of this parameter a key-value pairs, stored in an internal Thread "hashMap"

Within the get method still get this "hashMap" inside the current thread, then the current object this (ThreadLocal) as a key, get the value of conduct

 

 

 

 

 

 

 

 

 

 

 

 

We thought these two for a method to understand: 

Each thread may be running, you may operate a lot of ThreadLocal variables, how to distinguish between their own? 

Intuitive understanding is that we want to get the value of a ThreadLocal variable of a thread

A good solution is to be saved by means of Map, ThreadLocal variable as key, local value as the value

Assuming that the map name: threadLocalsMap, setter and getter methods may be provided to set and read internally 

threadLocalsMap.set(ThreadLocal key,T value) 

threadLocalsMap.get(ThreadLocal key) 

这样就是可以达到thread --- local的效果,但是是否存在一些使用不便?我们内部定义的是ThreadLocal变量,但是只是用来作为key的?是否直接通过ThreadLocal进行值的获取更加方便呢? 

怎么能够做到数据读取的倒置?因为毕竟值的确是保存在Thread中的

其实也很简单,只需要内部进行转换就好了,对于下面两个方法,我们都需要 ThreadLocal key 

threadLocalsMap.set(ThreadLocal key,T value) 

threadLocalsMap.get(ThreadLocal key)  

而这个key不就是这个ThreadLocal,不就是this 么 

所以: 

ThreadLocal.set(T value)就内部调用threadLocalsMap.set(this,T value) 

ThreadLocal.get()就内部调用threadLocalsMap.get(this)  

所以总结下就是: 

每个Thread内部保存了一个"hashMap",key为ThreadLocal,这个线程操作了多少个ThreadLocal,就有多少个key 

你想获取一个ThreadLocal变量的值,就是ThreadLocal.get(),内部就是hashMap.get(this); 

你想设置一个ThreadLocal变量的值,就是ThreadLocal.set(T value),内部就是hashMap.set(this,value); 

关键只是在于内部的这个“hashMap”,ThreadLocal只是读写倒置的“壳”,可以更简洁易用的通过这个壳进行变量的读写

“倒置”的纽带,就是getMap(t)方法

remove方法

 

 

 

 

 

 

 

 

 

remove方法也是简单,当前线程,获取当前线程的hashMap,remove

初始值

再次回头看下get方法,如果第一次调用时,指定线程并没有threadLocals,或者根本都没有进行过set 会发生什么? 

如下图所示,会调用setInitialValue方法

 

 

 

 

 

 

 

 

在setInitialValue方法中,会调用initialValue方法获取初始值,如果该线程没有threadLocals那么会创建,如果有,会使用这个初始值构造这个ThreadLocal的键值对 

简单说,如果没有set过(或者压根内部的这个threadLocals就是null的),那么她返回的值就是初始值

 

 

 

 

 

这个内部的initialValue方法默认的返回null,所以一个ThreadLocal如果没有进行set操作,那么初始值为null

image_5c64ccee_12c6

 

 

如何进行初始值的设定?

可以看得出来,这是一个protected方法,所以返回一个覆盖了这个方法的子类不就好了?在子类中实现初始值的设置 

在ThreadLocal中提供了一个内部类SuppliedThreadLocal,这个内部类接受一个函数式接口Supplier作为参数,通过Supplier的get方法获取初始值

 

 

 

 

 

 

 

 

 

Supplier是一个典型的内置函数式接口,无入参,返回类型T,既然是函数式接口也就是可以直接使用Lambda表达式构造初始值了!!!

 

 

 

 

 

 

如何构造这个内部类,然后进而进行初始化参数的设置呢? 

提供了withInitial方法,这个方法的参数就是Supplier类型,可以看到,这个方法将入参,透传给SuppliedThreadLocal的构造方法,直接返回一个SuppliedThreadLocal 

换句话说,我们不是希望能够借助于ThreadLocal的子类,覆盖initialValue()方法,提供初始值吗? 

这个withInitial就是能够达成目标的一个方法!

 

 

 

 

 

 

使用withInitial方法,创建具有初始值的ThreadLocal类型的变量,从结果可以看得出来,我们没有任何的设置,可以获取到值

image_5c64ccee_1240

 

 

 

 

 

 

 

稍作改动,增加了一次set和remove,从打印结果看得出来,set后,使用的值就是我们新设置的 

而一旦remove之后,那么仍旧会使用初始值

image_5c64ccee_47c5

 

 

 

 

 

 

 

注意:

对于initialValue方法的覆盖,其实即使没有提供这个子类以及这个方法也都是可以的,因为本质是要返回一个子类,并且覆盖了这个方法 

我们可以自己做,也可以直接匿名类,如下所示:创建了一个ThreadLocal的子类,覆盖了initialValue方法

ThreadLocal <类型 > threadLocalHolder =new ThreadLocal <类型> () { 

public 类型 initialValue() { return XXX; } 

};

但是很显然,提供了子类和方法之后,我们就可以借助于Lambda表达式进行操作,更加简介

4. 总结

通过set方法可以进行值的设定 

通过get方法可以进行值的读取,如果没有进行过设置,那么将会返回null;如果使用了withInitial方法提供了初始值,将会返回初始值 

通过remove方法将会移除对该值的写入,再次调用get方法,如果使用了withInitial方法提供了初始值,将会返回初始值,否则返回null 

对于get方法,很显然如果没有提供初始值,返回值为null,在使用时是需要注意不要引起NPE异常

ThreadLocal,thread  local,每个线程一份,到底是什么意思? 

他的意思是对于一个ThreadLocal类型变量,每个线程有一个对应的值,这个值的名字就是ThreadLocal类型变量的名字,值是我们set进去的变量

但是如果set设置的是共享变量,那么ThreadLocal其实本质上还是同一个对象不是么?

这句话如果有疑问的话,可以这么理解 

对于同一个ThreadLocal变量a,每个线程有一个map,map中都有一个键值对,key为a,值为你保存的值 

但是这个值,到底每个线程都是全新的?还是使用的同一个?这是你自己的问题了!!! 

ThreadLocal可以做到每个线程有一个独立的一份值,但是你非得使用共享变量将他们设置成一个,那ThreadLocal是不会保障的 

这就好比一个对象,有很多引用指向他,每个线程有一个独立的引用,但是对象根本还是只有一个 

所以,从这个角度更容易理解,为什么说ThreadLocal并不是为了解决线程安全问题而设计的,因为他并不会为线程安全做什么保障,他的能力是持有多个引用,这多个引用是否能保障是多个不同的对象,你来决策

所以我们最开始说的,ThreadLocal会为每个线程创建一个变量副本的说法是不严谨的

是他有这个能力做到这件事情,但是到底是什么对象,还是要看你set的是什么,set本身不会对你的值进行干涉

不过我们通常就是在合适的场景下通过new对象创建,该对象在线程内使用,也不需要被别的线程访问

如下图所示,你放进去的是一个共享变量,他们就是同一个对象

image_5c64ccef_3c7b

 

 

 

 

 

 

 

 

 

 

 

 

5. 应用场景

前面说过,对于之前生产者消费者的示例中,就不适合使用ThreadLocal,因为问题模型就是要多线程之间协作,而不是为了线程安全就将共享变量私有化 

比如,银行账户的存款和取款,如果借助于ThreadLocal创建了两个账户对象,就会有问题的,初始值500,明明又存进来1000块,可支配的总额还是500 

那ThreadLocal适合什么场景呢? 

既然是每个线程一个,自然是适合那种希望每个线程拥有一个的那种场景(好像是废话) 

一个线程中一个,也就是线程隔离,既然是一个线程一个,那么同一个线程中调用的方法也就是共享了,所以说,有时,ThreadLocal会被用来作为参数传递的工具

因为它能够保障同一个线程中的值是唯一的,那么他就共享于所有的方法中,对于所有的方法来说,相当于一个全局变量了! 

所以可以用来同一个线程内全局参数传递

不过要慎用,因为“全局变量”的使用对于维护性、易读性都是挑战,尤其是ThreadLocal这种线程隔离,但是方法共享的“全局变量”

如何保障必然是独立的一个私有变量?

对于ThreadLocal无初始化设置的变量,返回值为null 

所以可以进行判断,如果返回值为null,可以进行对象的创建,这样就可以保障每个线程有一个独立的,唯一的,特有的变量

示例:

对于JavaWeb项目,大家都了解过Session 

ps:此处不对session展开介绍,打开浏览器输入网址,这就会建立一个session,关闭浏览器,session就失效了 

在这一个时间段内,一个用户的多个请求中,共享同一个session 

Session 保存了很多信息,有的需要通过 Session 获取信息,有些又需要修改 Session 的信息 

每个线程需要独立的session,而且很多地方都需要操作 Session,存在多方法共享 Session 的需求,所以session对象需要在多个方法中共享 

如果不使用 ThreadLocal,可以在每个线程内创建一个 Session对象,然后在多个方法中将他作为参数进行传递 

很显然,如果每次都显式的传递参数,繁琐易错 

这种场景就适合使用ThreadLocal

下面的示例就模拟了多方法共享同一个session,但是线程间session隔离的示例

public class T24 {
    /**
     * session变量定义
     */
    static ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
    /**
     * 获取session
     */
    static Session getSession() {
        if (null == sessionThreadLocal.get()) {
            sessionThreadLocal.set(new Session());
        }
        return sessionThreadLocal.get();
    }
    /**
     * 移除session
     */
    static void closeSession() {
        sessionThreadLocal.remove();
    }
    /**
     * 模拟一个调用session的方法
     */
    static void fun1(Session session) {
    }
    /**
     * 模拟一个调用session的方法
     */
    static void fun2(Session session) {
    }
    public static void main(String[] args) {
        new Thread(() -> {
            fun1(getSession());
            fun2(getSession());
            closeSession();
        }).start();
    }
    /**
     * 模拟一个session
     */
    static class Session {
    }
}

所以,ThreadLocal最根本的使用场景应该是:

在每个线程希望有一个独有的变量时(这个变量还很可能需要在同一个线程内共享)

避免每个线程还需要主动地去创建这个对象(如果还需要共享,也一并解决了参数来回传递的问题)

换句话说就是,“如何优雅的解决:线程间隔离与线程内共享的问题”,而不是说用来解决乱七八糟的线程安全问题

所以说如果有些场景你需要线程隔离,那么考虑ThreadLocal,而不是你有了什么线程安全问题需要解决,然后求助于ThreadLocal,这不是一回事

既然能够线程内共享,自然的确是可以用来线程内全局传参,但是不要滥用

再次注意:

ThreadLocal只是具有这样的能力,是你能够做到每个线程一个独有变量,但是如果你set时,不是传递的new出来的新变量,也就只是理解成“每个线程不同的引用”,对象还是那个对象(有点像参数传递时的值传递,对于对象传递的就是引用)

6. 内存泄漏 

ThreadLocal很好地解决了线程数据隔离的问题,但是很显然,也引入了另一个空间问题

如果线程数量很多,如果ThreadLocal类型的变量很多,将会占用非常大的空间

而对于ThreadLocal本身来说,他只是作为key,数据并不会存储在它的内部,所以对于ThreadLocal

ThreadLocalMap内部的这个Entity的key是弱引用

 

 

 

 

 

 

 

 

 

 

如下图所示,实线表示强引用,虚线表示弱引用

对于真实的值是保存在Thread里面的ThreadLocal.ThreadLocalMap threadLocals中的

借助于内部的这个map,通过“壳”ThreadLocal变量的get,可以获取到这个map的真正的值,也就是说,当前线程中持有对真实值value的强引用

而对于ThreadLocal变量本身,如下代码所示,栈中的变量与堆空间中的这个对象,也是强引用的

  static ThreadLocal<String> threadLocal = new ThreadLocal<>();

不过对于Entity来说,key是弱引用

image_5c64ccef_3050

 

 

 

 

 

 

当一系列的执行结束之后,ThreadLocal的强引用也会消亡,也就是堆与栈之间的从ThreadLocal Ref到ThreadLocal的箭头会断开

由于Entity中,对于key是弱引用,所以ThreadLocal变量会被回收(GC时会回收弱引用)

而对于线程来说,如果迟迟不结束,那么就会一直存在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value的强引用,所以value迟迟得不到回收,就会可能导致内存泄漏 

ThreadLocalMap的设计中已经考虑到这种情况,所以ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value

以get方法为例

image_5c64ccef_48c7

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

一旦将value设置为null之后,就斩断了引用于真实内存之间的引用,就能够真正的释放空间,防止内存泄漏

image_5c64ccef_751d

但是这只是一种被动的方式,如果这些方法都没有被调用怎么办?

而且现在对于多线程来说,都是使用线程池,那个线程很可能是与应用程序共生死的,怎么办?

那你就每次使用完ThreadLocal变量之后,执行remove方法!!!!

从以上分析也看得出来,由于ThreadLocalMap的生命周期跟Thread一样长,所以很可能导致内存泄漏,弱引用是相当于增加了一种防护手段

通过key的弱引用,以及remove方法等内置逻辑,通过合理的处理,减少了内存泄漏的可能,如果不规范,就仍旧会导致内存泄漏

7.总结

ThreadLocal可以用来优雅的解决线程间隔离的对象,必须主动创建的问题,借助于ThreadLocal无需在线程中显式的创建对象,解决方案很优雅

ThreadLocal中的set方法并不会保障的确是每个线程会获得不同的对象,你需要对逻辑进行一定的处理(比如上面的示例中的getSession方法,如果ThreadLocal 变量的get为null,那么new对象)

是否真的能够做到线程隔离,还要看你自己的编码实现,不过如果是共享变量,你还放到ThreadLocal中干嘛?

所以通常都是线程独有的对象,通过new创建

发布了70 篇原创文章 · 获赞 7 · 访问量 1万+

Guess you like

Origin blog.csdn.net/qiang_zi_/article/details/104496561