Java并发——ThreadLocal总结

概述

并发问题,有时候,可以用ThreadLocal方式来避免。

ThreadLocal,顾名思义,就是线程自己的,独享的,就像线程栈是线程独享的一样。

本文讨论三点:

  1. 基本用法
  2. 设计原理
  3. 父子线程

基础用法

考虑类A有doSync方法,可能会被并发调用. 因为SimpleDateFormat非线程安全,所以在方法内new创建。

public void doSync(){
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    simpleDateFormat.format(new Date());
    // .... some complex ops
}
复制代码

可以优化为以下方案,让每个线程都有自己的SimpleDateFormat, 从而不用每次调用都new一个:

private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
    SimpleDateFormat simpleDateFormat = sdf.get();
    simpleDateFormat.format(new Date());
    // .... some complex ops
}
复制代码

说明:本段代码可能在某个类A中, 方法doSync可能会被并发调用。

如果在doSync内部new一个SimpleDateFormat,同一个线程调用也要每次都new一个,有损性能,其实同一个线程可以共享一个。所以,可以用一个ThreadLocal类型的变量,包含一个SimpleDateFormat。 这里没有调用remove,是希望每个线程里都常驻一个日期格式化对象。

另外的一个栗子是,我们在web开发里,有时候会跨层传播一些上下文信息,会使用ThreadLocal,譬如在某个filter里使用set方法设置,然后结束的时候remove。

ThreadLocal主要方法说明:

  • withInitial : 接受一个Supplier(函数接口,定义了get方法,顾名思义,就是提供者),提供什么?当然是提供要放在ThreadLocal内的变量,因为是要在线程内创建,不是马上要,所以需要的是一个supplier
  • set: 设置当前线程ThreadLocal包含的值
  • get: 获取当前线程ThreadLocal包含的值
  • remove:移除当前线程ThreadLocal包含的值

设计原理

为了更加直观的感受ThreadLocal和ThreadLocal所容纳变量的关系,可以继续看下图。 ThreadLocal仅仅是一个访问者,线程独占的变量在各自线程的ThreadLocalMap中。

不过需要注意的是,图中,ThreadLocal对象T1本身的引用,有对象A,线程1,线程2,线程3一共4个持有者。

我们还是用上文日期格式化的代码来说明对应关系:

 // 类A的代码片段
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
    SimpleDateFormat simpleDateFormat = sdf.get();
    simpleDateFormat.format(new Date());
    // .... some extremely complex ops
	  
}
复制代码
  • 变量X: 就是 ThreadLocal.withInitial 里面 Supplier方法的返回值,一个SimpleDateFormat对象
  • ThreadLocal对象T1: 就是类A里定义的成员变量 ThreadLocal sdf
  • ThreadLocalMap: 一种Map数据结构,类似HashMap,线程框架里自己实现一个Map,应该是不想和集合框架耦合吧

问题1: 为什么要用一个Map呢?

因为这种A对象可能有很多个,变量X,ThreadLocal对象T1都会有很多个。

问题2: 有人说ThreadLocal有内存泄漏,是什么意思?

首先我们明确一下内存泄漏: 不会再使用的对象或者变量,占用着内存,且无法被GC掉,称为内存泄漏。 ThreadLocal在线程的ThreadLocalMap中,Key是ThreadLocal对象, Value是变量X副本,泄漏的可能是Key和Value。

案例中的日期格式化工具,仅仅在A的代码片段里有用,而当A对象GC-Root不可达要被干掉了,ThreadLocal对象T1的强引用sdf就没有了,而线程1,2,3里的各自ThreadLocalMap中还有。当不规范使用的时候,或者就是倔强,不remove。久而久之,就会有很多无用的Key和Value充斥着ThreadLocalMap。

但是呢,倔强的我回想了一下,其实往往都没事,这么久了,我都没删啊,也没遇到泄漏啊!

作为框架设计者自然会考虑到,为了方便这些上帝使用框架(Java程序员),从2点分别针对Key和value的泄漏:

  1. 使用ThreadLocal弱引用作为Key,当ThreadLocal变量只有弱引用时,就会被GC掉,ThreadLocalMap里的key就会指向null(或者说Key就是null)
  2. ThreadLocalMap当rehash的时候,会干掉key为null对应的Value (这或许也是自己实现一个Map的原因吧)

所以,如果没有rehash,泄漏还是存在的,只不过,一般很难达到觉察的程度。

下面,从源码的角度佐证一下针对泄漏所做的2个要点。

弱引用:

// ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);   // ThreadLocal k 在这里开始被弱引用指向了
        value = v;
    }
}
复制代码

清理key为null的value:

// ThreadLocal.ThreadLocalMap.rehash
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}


// ThreadLocal.ThreadLocalMap.resize
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;
}

复制代码

父子线程

有时候,执行业务逻辑需要异步,但是当前线程的ThreadLocal变量,怎么传递给子线程呢?

ThreadLocal有个子类InheritableThreadLocal, 基本使用如下:

static class A {
    private InheritableThreadLocal<HashMap<String, String>> map1 = new InheritableThreadLocal<HashMap<String, String>>(){
        @Override
        protected HashMap<String, String> initialValue() {
            return new HashMap<>(8);
        }
    };
    private ThreadLocal<HashMap<String, String>> map2 = new ThreadLocal<HashMap<String, String>>(){
        @Override
        protected HashMap<String, String> initialValue() {
            return new HashMap<>(8);
        }
    };
    public void doAsync(){
        map1.get().put("name", "zhangsan");
        map2.get().put("name", "zhangsan");
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("map1: " + map1.get().get("name"));  // 子线程t可以读取到map1的name
                System.out.println("map2: " + map2.get().get("name"));  // 却无法读取到map2的name
            }
        }, "A-SUB-0");

        t.start();
    }
}

复制代码

怎么传递的呢?

创建线程的时候,Thread类的构造函数会判断当前线程中是否存在InheritableThreadLocal, 如果有,就会拷贝一份。

// Thread类构造函数执行的代码片段: 体现了对inheritableThreadLocal的复制
if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
复制代码

仅仅是在创建线程的时候,会发生一次拷贝, 拷贝的是ThreadLocalMap里的Entry数组,即包含Key:ThreadLocal对象和Value对象。

  • 后续父线程内有增减ThreadLocal,都和子线程无关。所以和线程池结合使用的时候,需要特别注意一下。
  • Key和Value都是引用拷贝,所以,同一个ThreadLocal Key和对应的Value变化,父子线程是共享的

伏笔

写到这里,发现还遗漏了一个知识点,就是ThreadLocalMap这个数据结构怎么实现的。

可以带着这几个问题去看下源码,本文暂时先留下伏笔吧~

  • 如何仅仅用一个数组来实现Map
  • 如何解决hash冲突
  • 如何扩容?
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值