概述

并发问题,有时分,能够用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。

Java并发——ThreadLocal总结

ThreadLocal主要办法阐明:

  • withInitial : 承受一个Supplier(函数接口,界说了get办法,望文生义,便是供给者),供给什么?当然是供给要放在ThreadLocal内的变量,由于是要在线程内创立,不是立刻要,所以需求的是一个supplier
  • set: 设置当时线程ThreadLocal包括的值
  • get: 获取当时线程ThreadLocal包括的值
  • remove:移除当时线程ThreadLocal包括的值

规划原理

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

不过需求留意的是,图中,ThreadLocal目标T1自身的引证,有目标A,线程1,线程2,线程3一共4个持有者。

Java并发——ThreadLocal总结

咱们仍是用上文日期格式化的代码来阐明对应联系:

 // 类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个要点。

Java并发——ThreadLocal总结

弱引证:

// 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抵触
  • 怎么扩容?