多线程 - (四)ThreadLocal

什么是ThreadLocal

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

线程局部变量并不是Java的新发明,很多语言(如IBM IBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

常用方法

ThreadLocal类很简单,主要有4个方法,我们先来了解一下:

public void set(T value) {}
设置当前线程的线程局部变量的值。

public T get() {}
该方法返回当前线程所对应的线程局部变量。

public void remove() {}
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected T initialValue() {}
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

使用方式

package com.ahut.thread;

/**
 * @author cheng
 * @className: ThreadMain
 * @description:
 * @dateTime 2018/6/4 15:46
 */
public class ThreadMain implements Runnable {

    // 只声明一个ThreadLocal,不设置默认返回值
    private static ThreadLocal<String> threadLocalString1 = new ThreadLocal<>();

    // 声明一个ThreadLocal,并设置默认的返回值
    private static ThreadLocal<String> threadLocalString = new ThreadLocal<String>(){

        // 覆写方法,修改默认的返回值
        @Override
        protected String initialValue() {
            return "默认值";
        }

    };

    /**
     * @description: 主函数
     * @author cheng
     * @dateTime 2018/6/4 15:46
     */
    public static void main(String[] args) {

        // 测试获取默认值
        String str = threadLocalString.get();
        System.out.println(str);

        // 设置值
        threadLocalString.set("11111");

        // 获取值
        str = threadLocalString.get();
        System.out.println(str);

        // 删除值
        threadLocalString.remove();
        str = threadLocalString.get();
        System.out.println(str);

    }
}

执行结果:

默认值
11111
默认值

实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

public class ThreadLocal<T> {

}

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。例如下面的 set 方法:

    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 不为空
        if (map != null)
            // 储存到ThreadLocalMap中
            map.set(this, value);
        else
            // 先创建ThreadLocalMap,再储存
            createMap(t, value);
    }

    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 创建ThreadLocalMap,并放进去第一个值
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

调用 ThreadLocal 的 set 方法时,首先获取到了当前线程,然后获取当前线程维护的 ThreadLocalMap 对象,最后在 ThreadLocalMap 实例中添加上。如果 ThreadLocalMap 实例不存在则初始化并赋初始值。

这里看到 set 方法的第一个参数是 this , this 即指的是当前的 ThreadLocal 对象,会看上看的代码就是指的 threadLocalString 这个对象。而在 ThreadLocalMap 的 set 方法中会根据当前 ThreadLocal 对象实例,做一些操作和判断,最终实现赋值操作(具体参考源码)。

所以说,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。

get方法类似:

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        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();
    }

    // 设置初始值
    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;
    }

    // 返回初始值
    protected T initialValue() {
        return null;
    }

应用场景

直接定位到 ThreadLocal 的源码,可以看到源码注释中有很清楚的解释:它是线程的局部变量,这些变量只能在这个线程内被读写,在其他线程内是无法访问的。 ThreadLocal 定义的通常是与线程关联的私有静态字段(例如,用户ID或事务ID)。

变量有局部的还有全局的,局部变量没什么好说的,一涉及到全局,那自然就会出现多线程的安全问题,要保证多线程安全访问,不出现脏读脏写,那就要涉及到线程同步了。而 ThreadLocal 相当于提供了介于局部变量与全局变量中间的这样一种线程内部的全局变量。

总结了半天,发现使用场景说到底就概括成一个:就是当我们只想在本身的线程内使用的变量,可以用 ThreadLocal 来实现,并且这些变量是和线程的生命周期密切相关的,线程结束,变量也就销毁了。

所以说 ThreadLocal 不是为了解决线程间的共享变量问题的,如果是多线程都需要访问的数据,那需要用全局变量加同步机制。

例子:
在实际项目中,可以用来减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,因为servlet是单例多线程的,每个请求执行的操作都是同一个线程中。比如:可以用ThreadLocal来存每一次请求用户的信息,定义了一个类UserContext

public class UserContext{

    private static ThreadLocal<User> userContext = new ThreadLocal<>();

    public static setUser(User user){
        userContext.set(user);

    public static User getUser(){
        return userContext.get();
    }

    public static void remove(){
        return userContext.remove();
    }
}

当用户每次请求进来时,在拦截器中获取用户信息调用UserContext.setUser()将其放到userContext中,无论在哪我们只要调用UserContext.getUser()可以很轻松的获取到用户的信息,而不用在函数调用时一层一层的传递。同时在拦截器结束时调用UserContext.remove()移除掉即可。

内存泄漏

1
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

2
threadLocals变量是在线程内部的,故没有多个线程去访问它,所以不存在线程不安全的说法,同时只要线程被回收了就不会存在内存泄漏。

ThreadLocal对象被回收时(key为null),没有办法获取到value,而线程又不会被回收时则value一直占用空间导致内存泄漏。线程不会被回收的常见场景是线程池。

JDK在此做了一个优化,在调用get(),set(),remove()方法会做额外处理来清理ThreadLocalMap中key为null的value,以减少内存泄漏的影响。但是如果key未使用弱引用,即使ThreadLocal被回收了,key也不为null,也就是说是没法判断哪个value需要回收的,最终造成内存泄漏。所以此处的弱引用key是内存泄漏的一个优化处理方式。

注意

  • 使用 ThreadLocal 的时候,最好不要声明为静态的;
  • 使用完 ThreadLocal ,最好手动调用 remove() 方法,因为有可能出现内存泄漏问题,而且会影响业务逻辑;

猜你喜欢

转载自blog.csdn.net/qq_28988969/article/details/80596081