【Java并发编程】ThreadLocal的用法以及内部原理

苡仁 2020年04月12日 85次浏览

1. ThreadLocal的用途

  • ThreadLocal它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。

常见使用场景如下:

  • 场景1:每个线程需要一个独享的对象(通常是工具类,比如:SimpleDateFormatRandom) 我们就可以用ThreadLocal来保存对象。
  • 场景2:每个线程需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦(如:在拦截器中获取用户信息)

1.1 场景1:每个线程独享对象

  • 每个线程内有自己的实例副本,并且不共享

  • 假设大家都知道SimpleDateFormat是线程不安全的类,我们来尝试用ThreadLocal来让他变成线程安全类.

  1. 普通使用SimpleDateFormat做法
/**
 * @author yiren
 */
public class ThreadLocalSimple00 {

    public static String date(int seconds) {
        // 参数的单位是毫秒
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(ThreadLocalSimple00.date(10));
        }).start();
        new Thread(() -> {
            System.out.println(ThreadLocalSimple00.date(1007));
        }).start();
        new Thread(() -> {
            System.out.println(ThreadLocalSimple00.date(123123));
        }).start();
    }
}
1970-01-01 08:00:10
1970-01-01 08:16:47
1970-01-02 06:12:03

Process finished with exit code 0
  • 我们可以看到每个线程在使用的时候为了线程安全都需要创建一次SimpleDateFormat对象。并且多个线程每次都new很麻烦,如果线程有1000个呢?
  • 有的小伙伴会说我是使用静态对象来实现,但是请注意SimpleDateFormat在多线程下是不安全的。
  • 还有的小伙伴说,我们可以加锁实现,让每次format的时候同步。但是这样是会降低性能。
  1. 使用ThreadLocal结合线程池来保存SimpleDateFormat,避免重复创建,并使用线程池来减少线程的创建开销。
/**
 * @author yiren
 */
public class ThreadLocalSimple01 {

    private static ExecutorService executorService = Executors.newFixedThreadPool(8);
    private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

    public static String date(int seconds) {
        // 参数的单位是毫秒
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = simpleDateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            final int seconds = i;
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName() + ": " +ThreadLocalSimple01.date(seconds));
            });
        }
        executorService.shutdown();

    }
}
......
pool-1-thread-5: 1970-01-01 08:16:34
pool-1-thread-1: 1970-01-01 08:16:35
pool-1-thread-7: 1970-01-01 08:16:18
pool-1-thread-5: 1970-01-01 08:16:37
pool-1-thread-7: 1970-01-01 08:16:39
pool-1-thread-4: 1970-01-01 08:16:36
pool-1-thread-3: 1970-01-01 08:16:33
pool-1-thread-8: 1970-01-01 08:16:30
pool-1-thread-2: 1970-01-01 08:16:29
pool-1-thread-6: 1970-01-01 08:16:28
pool-1-thread-1: 1970-01-01 08:16:38

Process finished with exit code 0
  • 说明一下:初始化方法initialValue()方法在此场景内一般是需要重写的,如果不重写则返回的是null。后面会有源码分析。重写这个方法,返回一个需要保存副本变量的对象。在get的时候,他会去判断内部是否已经创建过这个对象,如果没有,那就会调用initialValue()这个方法来初始化

  • 除了上面的写的方式创建ThreadLocal我们还可以利用java8中ThreadLocal提供的withInitial的方式来写,如下:

private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
  1. 我们为什么要这样使用?
    • 首先new Thread()改成线程池,避免线程频发创建。
    • 不使用加锁的方式实现是因为加锁会大大降低性能
    • 使用ThreadLocal可避免重复创建多个对象,而且在线程池中有核心线程,它也就只会为每个线程创建一次对象。

1.2 场景2:保存全局变量,避免参数传递

  • 使用ThreadLocal保存一些业务内容(如:用户权限信息、从用户系统获取到的用户名,ID等等)

  • 可能有人会说为什么不用全局Map来保存呢?

    • 首先如果用Map,那么肯定是需要用ConcurrentHashMap或者使用synchronized来做同步,同样性能损耗也高。
    • 其次是,你还需要去管理里面内容的创建和销毁,自己设计起来成本会很高。当一个请求进来,我们就需要往全局Map中去存储一个数据,请求结束我们就需要去销毁他。
  • 使用ThreadLocal的方式会更好,这样就没有synchronized同步,可以不影响性能达到不层层传递数据,并且也保证了线程安全

  • 在此场景下,我们不需要重写initialValue()方法,但是我们需要手动调用set()方法,案例如下:

/**
 * @author yiren
 */
public class ThreadLocalParam00 {

    private static ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        for (int i = 0; i < 10; i++) {
            final Long id = (long) i;
            executorService.execute(() -> {
                UserInfo userInfo = getUserInfo(id);
                System.out.println(Thread.currentThread().getName() + " request in, " + userInfo);
                userInfoThreadLocal.set(userInfo);
                business01();
                System.out.println(Thread.currentThread().getName() + " request out, " + userInfo);
            });
        }
    }

    /**
     * 模拟获取用户信息
     *
     * @param id
     * @return
     */
    private static UserInfo getUserInfo(Long id) {
        return new UserInfo(id, "name-" + id);
    }

    /**
     * 模拟业务方法1
     */
    private static void business01() {
        System.out.println(Thread.currentThread().getName() + " business01 in");
        business02();
    }


    /**
     * 模拟业务方法2
     */
    private static void business02() {
        UserInfo userInfo = userInfoThreadLocal.get();
        System.out.println(Thread.currentThread().getName() + " business02, " + userInfo);
        // 相关业务
        userInfo.name = "changeName-" + userInfo.id;
    }

    private static class UserInfo {
        Long id;
        String name;

        public UserInfo(Long id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
}
pool-1-thread-1 request in, UserInfo{id=0, name='name-0'}
pool-1-thread-1 business01 in
pool-1-thread-1 business02, UserInfo{id=0, name='name-0'}
pool-1-thread-1 request out, UserInfo{id=0, name='changeName-0'}

  • 以上结果是从输入中选取thread-1的打印
  • 我们可以清晰得看到数据在各个方法内的操作,我们并没有传任何参数,通过ThreadLocal来完成线程私有变量传递

1.3 ThreadLocal的两个作用

  1. 让需要用到的对象在线程间隔离,也就是说每个线程都有自己独立对象。修改不影响其他线程
  2. 在当前线程的任何地方都可以直接获取到,无需传参操作,直接调用get()方法

1.4 创建对象的时机

  1. initialValue()
  • 当我们在ThreadLocal第一次get的时候吧对象初始化好,对象初始化的时机可以交由我们自己去重写方法控制。
  1. set()
  • 当我们需要保存到ThreadLocal中的对象生成时机不由我们控制,如用户请求进来,需要把用户信息放入ThreadLocal中,我们就只能在业务代码中去调用set方法放入。

1.5 ThreadLocal的好处

  1. 线程安全
  2. 不需要加锁,效率高
  3. 高效利用内存、节省开销
  4. 可以避免传参,不需要每次都层层在方法参数上传递

2. ThreadLocal内部原理解析

2.1 initialValue()get()

  1. initialValue()方法
  • initialValue()方法会返回当前线程对应的“初始值”,并且只有在当前线程get调用过后才会触发,属于延迟加载。默认实现是一个返回null。源码如下:
    protected T initialValue() {
        return null;
    }
  1. get()方法
  • 通过get()方法来获取到当前线程保存的对象。
    public T get() {
        Thread t = Thread.currentThread();
        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();
    }
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • 我们可以看到get方法首先通过Thread类方法获取到了当前执行类,然后通过getMap(t)来获取了当前线程的THradLocalMap类对象threadLocals属性,我们可以看下threadLocals在Thread类中的定义:
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
  • 注释:与此线程相关的ThreadLocal值。这个mapThreadLocal类维护。
  • 如果获取当前线程的threadLocalsMap不为空,就会调用**map.getEntry(this)用当前的ThreadLocal对象作为key来获取对应的value对象**。如果能获取到就把他转成对应的类型返回。
  • 如果当前的线程里面threadlocals为空或者获取出来的value为空就会引发调用setInitialValue()方法
  • 我们看一下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;
}
  • 这个方法的第一行就调用了initialValue()方法返回对应的对象值
  • 然后获取到当前线程的threadLocals,如果不为空就把新的值放入,注意此时是以当前的threadLocalkey;如果为空就创建ThreadLoclMap然后把当前线程的新value放入
  • 最后返回当前线程的对象value

2.2 set()方法

  • 上面我们说了当线程第一次调用get方法,会间接触发initialValue方法。
  • 但是如果我们先调用了set方法,再调用get方法,此时就不会触发initialValue方法了。
  • 我们来看看set方法:
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • set方法其实就是和setInitialValue方法差不多的逻辑。获取到线程的threadLocals,然后判断是否为空,为空就创建并且放入,不为空就直接放入。
  • 所以调用set过后再调用get,此时get的里面获取的result就可以拿到,而不会去调用setInitialValue了。

2.3 remove()方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
  • 当然我们可以设置值也可以移除值。
  • 如果我们想移除某个线程的threadLocalvalue,我们就可以直接用ThreadLocal在这个线程内调用remove即可。
  • 此时需要注意,如果调用了remove,我们再调用get,就会触发setInitialValue方法调用initailValue

2.4 ThreadLocal中的ThraedLocalMap

  • ThreadLocalMap类,也就是真正存每个线程的对象的容器。每个Thread的对象中都有一个threadLocals属性。
  • 里面真正存储的是ThreadLocalMap中的内部类Entry的数组table`
/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;
  • 这个Entry我们可以看做一个类似于HashMap的集合容器的EntrySet,但是并不是像EntrySet一样实现的,而是用继承WeakReference(弱引用)类来实现的。它是使用线性探测法来解决Hash冲突的(也就是如果hash过后,如果数组不为空,那就到下一个位置去)。

  • 它的键是ThreadLocal的对象,值是我们想设定的对象值

3. ThreadLocal内存泄漏问题

3.1 问题详解

  • 内存泄漏:某个对象不再有用,但是占用的内存却不能被回收

  • 如果程序有很多内存泄漏,就会浪费很多空间造成OOM。

  • 发生内存泄漏的可能:要么是key,要么是value

    • ThreadLocalMap对象Entry的key ->ThreadLocal 是一个弱引用,是可以被回收的
    • 但是它的value是强引用,也就是说,只要当前线程终止,ThreadLocal米面的value会被垃圾回收,没有任何强引用了,就会被回收。
  • 弱引用的特点是:如果这个对象只被弱引用关联,那么这个对象时可以被回收的。

  • 但是如果线程始终不终止(比如线程池中),那么key对应的value就不会被回收。就会有如下关系:

Thread->ThreadLocalMap(threadLocals)->Entry(key为null)->Value

  • 如果以上关系不得到解决,这个强引用链路就存在,也就会导致value无法回收,随着ThreadLocal增多,就可能会出现OOM问题
  • JDK其实已经考虑了这个问题,所以在set、remove、rehash方法中,它会去扫描key为null的Entry,并把对应的value设置为null值来回收对象,以确保不会造成OOM

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

  • 可以看到,上面的resize方法中如果k==null就会执行e.value = null; // Help the GC把对应的value值设置成空,后面还注明了是帮助GC。感兴趣可以去看下setremoverehash方法内有对其他方法的调用中有对key==null的判断。
  • 但是如果一个ThreadLocal不被使用,那就这些方法就不会被调用,如果线程又不停止,那么引用链就一直存在,那么就导致了value的内存泄漏

3.2 如何避免内存泄漏

  • 阿里代码规范中就明确指出:

    • 我们在使用了ThreadLocal之后,就应该去调用remove方法,去主动得把Entry干掉!
  • 所以我们在上面的案例代码中,就应该使用了过后,调用remove,来避免内存泄漏

  • 在业务中,我们可以通过拦截器,AOP来完成

4. Spring中的应用举例--RequestContextHolder

  • 顾名思义,这个类使用来保存Request域相关的内容的。
  • 我们在编程的时候,通常会遇到有些人把requestservice传来获取一些请求信息,其实这个是必要的,我们可以直接通过RequestContextHolder来拿到Request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
  • RequestContextHolder里面就是使用ThreadLocal来实现的。

  • 文章内容来源:
  • 《Java并发编程艺术》、JDK1.8版本源码、慕课网悟空JUC课程