ThreadLocal探究

目录

 

一、ThreadLocal是什么

二、ThreadLocal怎么用

三、ThreadLocal源码分析

四、ThreadLocal其他几个注意的点


参考文章:

面试官没想到ThreadLocal我用得这么溜,人直接傻掉

再有人问你什么是ThreadLocal,就把这篇文章甩给他!

ThreadLocal,你想了解的都在这里(进来瞧瞧不后悔)

ThreadLocal理解及应用

 

一、ThreadLocal是什么

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

ThreadLocal发现除了系统源码的使用,很少在项目中用到,不过也还是有的。ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。例如:

Spring实现事务隔离级别的源码 

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,
ThreadLocal探究_第1张图片

Spring的事务主要是ThreadLocal和AOP去做实现的,每个线程自己的链接是靠ThreadLocal保存的。

 

另外,一个应用实例,假如我们有一个需求,那就是在多线程环境下,去格式化时间为指定格式yyyy-MM-dd HH:mm:ss,假设一开始只有两个线程需要这么做,代码如下:

public class ThreadLocalUsage01 {
 
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalUsage01().date(10);
                System.out.println(date);
            }
        }).start();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalUsage01().date(1000);
                System.out.println(date);
            }
        }).start();
    }
 
    private String date(int seconds) {
        // 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
 
}

在线程少的情况下是没有问题的,我们在每个线程里调用date方法,也就是在每个线程里都执行了创建SimpleDateFormat对象,每个对象在各自的线程里面执行格式化时间

但是我们是否会思考到,假如有1000个线程需要格式化时间,那么需要调用1000次date方法,也就是需要创建1000个作用一样的SimpleDateFormat对象,这样是不是太浪费内存了?也给GC带来压力?

于是我们联想到,1000个线程来共享一个SimpleDateFormat对象,这样SimpleDateFormat对象只需要创建一次即可

所以使用线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

ThreadLocal探究_第2张图片

ThreadLocal探究_第3张图片

public class ThreadLocalUsage04 {
 
    public static ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            THREAD_POOL.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalUsage04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        THREAD_POOL.shutdown();
    }
 
    private String date(int seconds) {
        // 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeDateFormatter.dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }
 
}
 
class ThreadSafeDateFormatter {
 
    public static ThreadLocal dateFormatThreadLocal = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
 
}

上面的代码使用到了ThreadLocal,将SimpleDateFormat对象用ThreadLocal包装了一层,使得多个线程内部都有一个SimpleDateFormat对象副本,每个线程使用自己的SimpleDateFormat,这样就不会产生线程安全问题了。

那么以上介绍的是ThreadLocal的第一大场景的使用,也就是利用到了ThreadLocal的initialValue()方法,使得每个线程内都具备了一个SimpleDateFormat副本。

接下来我们一起来看看ThreadLocal的第二大使用场景,在使用之前,我们先把两个场景总结如下:

  • 场景1:每个线程需要一个独享的对象,通常是工具类,比如典型的SimpleDateFormat和Random等。

  • 场景2:每个线程内需要保存线程内的全局变量,这样线程在执行多个方法的时候,可以在多个方法中获取这个线程内的全局变量,避免了过度参数传递的问题。

那么如何理解第二个问题呢?我们还是使用一个Demo来理解:假设有一个学生类,类成员变量包括姓名,性别,成绩,我们需要定义三个方法来分别获取学生的姓名、性别和成绩

从上面的代码中可以看出,每个类的方法都需要传递学生的信息才可以获取到正确的信息,这样做能达到目的

但是每个方法都需要学生信息作为入参,这样未免有点繁琐,且在实际使用中通常在每个方法里面还需要对每个学生信息进行判空,这样的代码显得十分冗余,不利于维护。

也许有人会说,我们可以将学生信息存入到一个共享的Map中,需要学生信息的时候直接去Map中取,如下图所示:

ThreadLocal探究_第4张图片

public class ThreadLocalUsage05 {
 
    public static void main(String[] args) {
        init();
        new NameService().getName();
        new SexService().getSex();
        new ScoreService().getScore();
    }
 
    private static void init() {
        Student student = new Student();
        student.name = "Lemon";
        student.sex = "female";
        student.score = "100";
        ThreadLocalProcessor.studentThreadLocal.set(student);
    }
 
}
 
class ThreadLocalProcessor {
 
    public static ThreadLocal studentThreadLocal = new ThreadLocal<>();
 
}
 
class Student {
 
    /**
     * 姓名、性别、成绩
     */
    String name;
    String sex;
    String score;
 
}
 
class NameService {
 
    public void getName() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().name);
    }
 
}
 
class SexService {
 
    public void getSex() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().sex);
    }
 
}
 
class ScoreService {
 
    public void getScore() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().score);
    }
 
}

ThreadLocal探究_第5张图片

ThreadLocal探究_第6张图片 

现在相信你已经对ThreadLocal有一个大致的认识了,下面我们看看如何用?

二、ThreadLocal怎么用

既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:

ThreadLocal探究_第7张图片

从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。

这就是TheadLocal的基本使用,是不是非常的简单。那么为什么会在数据库连接的时候使用的比较多呢?

ThreadLocal探究_第8张图片

上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。

这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。

以上主要是讲解了一个基本的案例,然后还分析了为什么在数据库连接的时候会使用ThreadLocal。下面我们从源码的角度来分析一下,ThreadLocal的工作原理。

三、ThreadLocal源码分析

在最开始的例子中,只给出了两个方法也就是get和set方法,其实还有几个需要我们注意。

这里写图片描述

ThreadLocal探究_第9张图片

方法这么多,我们主要来看set,然后就能认识到整体的ThreadLocal了

ThreadLocal探究_第10张图片

 

从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。

OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。

ThreadLocal探究_第11张图片

我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

还有一个getMap

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。

ThreadLocal探究_第12张图片

通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。

如何设置一个初始值呢?

ThreadLocal探究_第13张图片

ThreadLocal探究_第14张图片

从我们的map移除即可。

OK,其实内部源码很简单,现在我们总结一波

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap。

四、ThreadLocal其他几个注意的点

ThreadLocal探究_第15张图片

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

ThreadLocal探究_第16张图片

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(并发编程)