美文网首页javaalreadyjdk
[Java]重学Java-深入浅出ThreadLocal

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

作者: AbstractCulture | 来源:发表于2022-08-30 15:46 被阅读0次

ThreadLocal

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

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

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

代码示例

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

public class Test {
    //ThreadLocal<T>
    public static ThreadLocal<Long> 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<ThreadLocal<?>> {
        /** 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<String> 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<String> 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<T> extends ThreadLocal<T> {
    /**
     * 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.
     * <p>
     * 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是如何复用连接的,等等.

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

相关文章

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

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

  • Hello Java

    目录 Java-基础(1/6) Java-对象(2/6) Java-核心库类 上(3/6) Java-核心库类下(...

  • [Java]重学Java-继承

    复用 随着码代码的时间增长,程序员会越发需要"复用"这个能力,比如最简单的对String类进行判空: 我们需要每次...

  • [Java]重学Java-集合

    容器 很多时候,我们写程序需要进行批量的操作,比如说,新增一批学生列表.那么就需要有容器来装下这10个对象。Jav...

  • [Java]重学Java-接口

    接口是什么 首先接口不是类,它本身是无法被实例化的,只有实现类可以被实例化。它更像是一种功能的描述,程序输入什么值...

  • [Java]重学Java-多态

    从一个例子理解多态 对于一个不同特征的对象,使用接口可以隐藏子类的行为,对外暴露同一个接口,从而实现编程的解耦. ...

  • [Java]重学Java-可变参数

    可以变动的参数数量 编程离不开写方法(函数),Java中的大多数方法都是固定数量的参数,JDK5提供了可变参数来让...

  • [Java]重学Java-枚举类

    枚举可以做什么 假设我们现在想表达星期,为了避免魔法值的出现,我们可能会写一个常量类: 这样一来,1-7的星期数就...

  • [Java]重学Java-内部类

    什么是内部类 一个类中定义了另一个类,被定义的类叫作内部类。内部类可以将逻辑隐藏,使用好修饰符可以让实现逻辑对外隔...

  • [Java]重学Java-文档注释

    文档注释 刚开始学习Java的时候,觉得注释是很啰嗦的东西,但是随着编码的时间增长,觉得注释是很重要的东西。写好注...

网友评论

    本文标题:[Java]重学Java-深入浅出ThreadLocal

    本文链接:https://www.haomeiwen.com/subject/dgwlnrtx.html