目录

1、ThreadLocal是什么?

2、ThreadLocal实现原理

3、设置线程变量的2种方式

4、关于ThreadLocal的内存泄漏问题

5、使用过程中的注意事项和误区


1、ThreadLocal是什么?

    比较书面的回答:
类如其名,线程本地变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。这句话没问题,但容易被人误解,会被误以为:任意变量用ThreadLocal维护都是线程隔离的。后面会解答这个问题。

2、ThreadLocal实现原理

       每个线程(Thread)中都有一个 ThreadLocalMap容器,通过ThreadLocal可以存放和读取Thread中的ThreadLocalMap。每个Thread对象之间是隔离的,Thread对象中的ThreadLocalMap容器自然也是隔离的。
通俗点来说:可以把ThreadLocal看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap,实现互不干扰。

要探究原里,就离不开两个类:Thread和ThreadLocal,先分别看一下这两个类,为了方便理解,这里就简要介绍核心的概念和源码,更细节的东西查看源码就行,源码不多也很简单。

1)关于Thread类

如下代码判断,代码中的任意位置我们都可以通过 Thread.currentThread() 来获取当前线程对象,即可以获得当前线程的名称、id等等属性;但是无法直接获取到Thread中的ThreadLocalMap。

    public static void main(String[] args) {
        // 获取当前线程-主线程
        Thread mainThread = Thread.currentThread();
        System.out.println("main thread id: " + mainThread.getId());
        System.out.println("main thread name: " + mainThread.getName());

        System.out.println("------------------------------");

        // 自定义线程 1
        Thread thread1 = new Thread(()-> {
            // 获取当前线程
            Thread th = Thread.currentThread();
            System.out.println("thread id: " + th.getId());
            System.out.println("thread name: " + th.getName());
        });
        // 设置线程名称
        thread1.setName("MyThread-1");
        thread1.start();
    }

执行结果:

看Thread类的源码,里面有一个属性ThreadLocalMap,该Map就是用来存储各线程独立变量的。

2) 关于ThreadLocal类
    前面说了,可以把它看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap。

简单看一下ThreadLocal类的源码

Thread类中的ThreadLocalMap属性是ThreadLocal类中的内部类

从源码可以看出,ThreadLocal类中有内部类ThreadLocalMap,ThreadLocalMap中有内部类Entry,Entry类有两个属性,k和v。ThreadLocalMap是用的Entry数组来存储数据(Entry对象)。

使用ThreadLocal的set方法添加一个变量,下面通过代码来看一下这个流程

    public static void main(String[] args) {
        ThreadLocal<User> threadLocal = new ThreadLocal<>();
        // 创建线程1
        Thread thread1 = new Thread(()->{
            User user = new User("user1");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
        });
        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();
    }

ThreadLocal.set()方法的源码:

    public void set(T value) {
        // 得到当前线程对象
        Thread t = Thread.currentThread();
        // 得到当前线程对象中的Map
        ThreadLocalMap map = getMap(t);
        // Map不为空就把值添加进去,this就是ThreadLocal对象,如果为空就创建一个Map
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

ThreadLocal.get()也是类似的道理,先拿到当前线程对象,再拿到当前线程对象中的ThreadLocalMap,再从中取值。

最终还是应了开头那句话,可以把ThreadLocal看着一个工具类,可以用他来往当前线程中存储和获取值。

3、设置线程变量的2种方式
 

1)创建ThreadLocal对象时设置变量

创建ThreadLocal对象时设置初始化值,通过执行结果可以看出,每个线程在第一次调用get方法获取值的时候都会执行该段代码初始化变量,也就是每个线程得到的是一个新的对象,最终都存储到自己线程Thread的ThreadLocalMap容器中,不是同一个对象,也不是同一个存储容器,当然是隔离的。
@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 创建ThreadLocal,并设置初始化值
        ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> {
            // 每个线程在不执行set方法设置变量的情况下,第一次调用get方法获取值的时候执行该段代码,初始化变量,也就是每个线程得到的是一个新的对象
            User user = new User("user1");
            return user;
        });

        // 创建线程1
        Thread thread1 = new Thread(()->{
            System.out.println("thread1 .......");
            threadLocal.get().setUserName("T1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });
        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        // 创建线程2
        Thread thread2 = new Thread(()->{
            System.out.println("thread2 .......");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });
        thread2.setName("thread2");
        thread2.start();
    }
}


2)通过set()方法设置变量

创建threadLocal对象,不设置初始化值,在各自的线程中通过set方法设置变量。

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 创建threadLocal对象,不设置初始化值
        ThreadLocal<User> threadLocal = new ThreadLocal<>();

        // 创建线程1
        Thread thread1 = new Thread(()->{
            User user = new User("user1");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });


        // 创建线程2
        Thread thread2 = new Thread(()->{
            User user = new User("user2");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

执行结果:

4、关于ThreadLocal的内存泄漏问题

提到ThreadLocal,肯定都会想到内存泄漏,当ThreadLocalMap的Entry中的key为null,而value不为null时,该value就永远不能被访问到,就是一个无用的对象,按理来说应该被回收,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,如果当前线程没有结束,当前线程持有ThreadLocalMap,ThreadLocalMap持有Entry对象,Entry对象包含value,value可达从而不会被回收掉,这样就存在了内存泄漏。

1)话接上面,为什么ThreadLocalMap的Entry中的key会为null呢?

因为Entry中的key是弱引用,在垃圾回收的时候,如果key没有被其他对象引用,也就是说后续代码中不会再被用到,他就会被回收,最终Entry中的key为null。原来ThreadLocal对象在这里被引用,现在key为空,ThreadLocal在这里就没有被引用,如果其他地方也没有引用ThreadLocal对象,ThreadLocal对象就可以被回收,释放内存。

在使用完ThreadLocal后调用其remove方法,就可以清除不被使用的变量,避免内存泄漏。

        Thread thread2 = new Thread(()->{
            User user = new User("user2");
            // 添加当前线程的变量,和其他线程隔离
            threadLocal.set(user);
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
            // 用完后清除,避免内存泄漏
            threadLocal.remove();
        });

在ThreadLocal中,调用get、set、remove方法都会清除key为空的value,避免内存泄漏。

通过源码,可以追踪到 expungeStaleEntry 方法,该方法会清空key为空的value。



2)既然key是弱引用,GC回收会影响ThreadLocal的正常工作吗?

不会,因为有ThreadLocal变量引用着它,也就是说后面还会用到他,是不会被GC回收的,执行一段代码一探究竟。
 

        Thread thread1 = new Thread(()->{
            // 设置变量
            threadLocal.set(new User("thread1-user"));
            // 输出变量
            System.out.println(threadLocal.get().getUserName());
            System.gc(); //垃圾回收
            System.out.println("gc...gc");
            // 输出变量
            System.out.println(threadLocal.get().getUserName());
        });

执行结果:

可以看到,如果后续还会用到,是不会被回收的,不然问题就大了:“上一秒刚设置的变量,下一秒获取的时候就没了?”。



5、使用过程中的注意事项和误区

1)ThreadLocal与线程池

一般web容器,如tomcat就使用了线程池,或者我们自定义的线程池,线程池中的线程是存在复用情况的。如果我们在当前线程中使用ThreadLocal设置了一个变量,】并且没有执行remove方法,当前线程执行结束后,线程还在线程池中存在,线程并没有被销毁,下一个请求过来就会使用线程池中的线程,就会拿到上一个请求在线程中设置的变量。所以使用玩后一定要调用ThreadLocal的remove方法。

2)错误的理解导致使用方法
很多人看见这句话:“用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程”,就会理解为变量或者内存的完全隔离,就会出现错误的用法,例如:

错误方式一

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<User> threadLocal = new ThreadLocal<>();
        // 创建线程1
        Thread thread1 = new Thread(()->{
            // 设置变量
            threadLocal.set(user);
            // 线程1改变了user对象的值
            threadLocal.get().setUserName("thread1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 创建线程2
        Thread thread2 = new Thread(()->{
            // 设置变量
            threadLocal.set(user);
            // 可拿到线程1中改变的user对象的值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

错误方式二

@Data
class User {
    private String userName;
    public User() {}
    public User(String userName) {
        System.out.println("init user...");
        this.userName = userName;
    }
}

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
        // 创建线程1
        Thread thread1 = new Thread(()->{
            // 线程1改变了user对象的值
            threadLocal.get().setUserName("thread1-user");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 创建线程2
        Thread thread2 = new Thread(()->{
            // 可拿到线程1中改变的user对象的值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        });

        // 设置名称、启动
        thread1.setName("thread1");
        thread1.start();

        Thread.sleep(1000);

        thread2.setName("thread2");
        thread2.start();
    }
}

执行结果:

是不是意外,不是说变量在线程之间是隔离的吗?怎么线程1改了user对象的值,线程二中也随之改变了呢?

因为ThreadLocal设置变量(对象)的时候,并不是拷贝一份新的变量(对象),而是直接赋值对象的引用,如果这个变量(对象)是一个公共变量(对象),那么各线程的ThreadLocalMap中的key所指向的其实还是同一个对象,并没有隔离。

代码说明:

public class Test {
    // 这是一个公共的变量
   public static User user = new User("user1");
   
   // ***** 线程不隔离
   // 不能到达user对象在各线程中互相隔离的效果, 因为user本身就是公共变量
   public static ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
   
    // ***** 线程隔离
   public static ThreadLocal<User> threadLocal2 = ThreadLocal.withInitial(()-> {
        // 每个线程在不执行set方法设置变量的情况下,第一次执行get方法的时候都会执行本段代码,创建新的对象,现场之间使用的就不是同一个user对象
        User user = new User("user1");
        return user;
    });
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部