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

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是如何复用连接的,等等.

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容