[Java]重学Java-深入浅出ThreadLocal

ThreadLocal

解决数据一致性的问题通常有几种方式(笔者理解为,进程内出现线程不安全的问题也是导致了数据不一致):

  1. 排队,典型的案例是synchronizedLock.
  2. 线程本地变量——ThreadLocal.
  3. 投票,可以了解一下著名的paxos算法

ThreadLocal可以让线程只访问自己线程的变量,避免了发生线程安全问题.同时,它对操作系统的开销更小,同步往往需要消耗操作系统的内核资源;但如果是ThreadLocal,它只需要内存进行存储即可。

代码示例

package com.tea.modules.java8.thread.threadLocal;

public class Test {
    //ThreadLocal
    public static ThreadLocal x = ThreadLocal.withInitial(() -> {
        // 延迟加载,只在第一次get的时候进行初始化
        System.out.println(Thread.currentThread().getId() + "initialValue run...");
        return Thread.currentThread().getId();
    });

    public static void main(String[] args) {
        x.get();
        // ThreadLocal为每一个线程存储一个独立的变量
        new Thread(() -> {
            x.set(107L);
            System.out.println(x.get());
        }).start();
        // ThreadLocal为每一个线程存储一个独立的变量
        new Thread(() -> {
            x.set(108L);
            System.out.println(x.get());
        }).start();
        // 清空当前线程的ThreadLocal的值
        x.remove();
    }
}
  • 输出结果
1initialValue run...
107
108

可以看到,同时开启了2个线程,分别对x变量进行设值,输出的都是各自设置的值,说明threadlocal是"线程隔离"的,可以保证线程安全.

ThreadLocal实现原理

Thread中的threadLocals

在Thread类中,有个属性叫threadLocals,它的类型是ThreadLocal.ThreadLocalMap.ThreadLocalMap是定制化的HashMap,它负责存储ThreadLocal设置的值.
也就是说,实际上ThreadLocal的set过程是这样的:

threadlocal

这里,ThreadLocal充当的是Key的作用,也就是引用,真正的值存放线程对象的内存空间里面.

  • java.lang.ThreadLocal#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);
}

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();
}
  1. 首先获取当前线程对象
  2. 获取对应的threadLocals
  3. 根据ThreadLocal获取map里面的值
  4. 如果获取到返回对象
  5. 如果获取不到,会根据当前threadLocals是否为空觉得是否进行初始化.

remove过程

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
  1. 首先获取当前线程对象
  2. 获取对应的threadLocals
  3. 清空当前ThreadLocal引用的值

ThreadLocal容易引发的问题

内存泄露

ThreadLocal其实是操作Thread中的threadLocals,如果当前线程不消亡,那么这些本地变量会一直存在,可能会造成内存溢出,因此最好的建议是,每次用完ThreadLocal我们都手动执行remove操作。

内存泄漏,程序申请内存后,没有释放已申请的内存空间,这部分空间的堆积终将导致内存溢出。

这里会涉及到ThreadLocalMap的设计,我们来看看它的Entry:

  • java.lang.ThreadLocal.ThreadLocalMap.Entry
    static class Entry extends WeakReference> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }

WeakReference,是弱引用的意思,当一个对象仅仅被WeakReference指向, 而没有任何其他strongReference指向的时候, 如果GC运行, 那么这个对象就会被回收。如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象。
假设我们没有主动调用remove方法, 那么回收过程可能是这样的:

GC

GC回收只回收了ThreadLocal引用,而value值仍未从内存空间中清理出去

因此,最妥当的方法还是手动调用remove方法. 因为我们的项目往往采用线程池(如果是tomcat容器也有所谓的工作线程),线程往往是循环利用的。

在多线程环境下,不支持继承性

有这么一个应用场景,如果我们希望在两个线程之间去使用ThreadLocal进行传值,ThreadLocal是不支持的.

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

public static void main(String[] args) {
    threadLocal.set("hello world");
    new Thread(() -> {
        System.out.println("thread:" + threadLocal.get());
    }).start();
    System.out.println("main:" + threadLocal.get());
}

因为ThreadLocal只绑定当前线程,那么在这种情况下,我们又希望有个东西能支持多线程之间去共享值,怎么做?——InheritableThreadLocal.

public static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
    threadLocal.set("hello world");
    new Thread(() -> {
        System.out.println("thread:" + threadLocal.get());
    }).start();
    System.out.println("main:" + threadLocal.get());
}  

InheritableThreadLocal提供了子线程去访问父线程ThreadLocal的能力.

public class InheritableThreadLocal extends ThreadLocal {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * 

* This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

从类的定义上看,InheritableThreadLocal绑定的,是Thread类的inheritableThreadLocals属性.
那么值是什么时候设置进这个inheritableThreadLocals变量的,我们继续看看Thread类的代码:

  • java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        Thread parent = currentThread();
        // 省略
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // 省略
    }

init方法的触发时机是在每个线程创建的阶段,他首先会获取当前线程对象(父线程),然后判断当前线程对象的inheritableThreadLocals是否为空,如果不为空,将父线程的inheritableThreadLocals包装为Map结构赋值给即将创建的线程的inheritableThreadLocals变量.

结束了么

学而不思则罔,我们学习了技术,看到了JDK的一些底层实现逻辑,但是实际上项目应用,我们是否真正能用到这些东西,以下是我经常遇到的场景:

  1. 多线程环境下,线程会被复用,InheritableThreadLocal是否也有问题,比如说,他只是存储了创建线程的那一刻的快照值,那么在后面的事件中,如果值发生变化,我们怎么去跟踪?
  2. 分布式环境下,ThreadLocal能给我们怎样的参考,比如在rpc中我们需要将一些信息进行传递,这些信息能否也有一个rpcContext的东西进行传递?
  3. 中间件框架对于ThreadLocal的一些用法,比如Spring的是怎么处理事务的、Mybatis是如何复用连接的,等等.

博主目前还没能完全参透这些,但是有一些参考资料,希望能给到大家一些灵感:

  • TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了
  • 吊打 ThreadLocal!
  • ThreadLocal系列之——父子线程传递线程私有数据(四)

你可能感兴趣的:([Java]重学Java-深入浅出ThreadLocal)