ThreadLocal 关键源码阅读及思考

前言

使用 ThreadLocal 类来定义变量能确保每个线程访问的是该变量的副本,这样就能避免多线程访问一个变量时产生的线程安全问题。

使用步骤

  1. 创建 ThreadLocal 对象

    • 普通创建方式
      ThreadLocal<Long> createdTime = new ThreadLocal<>();
    • 带有初始值的创建方式
      • 方式一
        ThreadLocal<Long> createdTime = ThreadLocal.withInitial(() -> System.currentTimeMillis());
      • 方式二
      ThreadLocal<Long> createdTime = new ThreadLocal<> () {
      	@Override
      	protected Long initialValue() {
      		return System.currentTimeMillis();
      	}
      };
      
  2. 设置值
    createdTime.set(System.currentTimeMillis());

  3. 取值
    long threadCreatedTime = createdTime.get(();

线程安全原理

在 Java 中,每个线程在存活期间都隐式地持有一个或多个 ThreadLocal 变量的引用,而在该线程退出后,其它引用也没有指向该线程所对应的 ThreadLocal 变量时,这些 ThreadLocal 变量就会被 垃圾回收器 回收。

Thread 维护着 ThreadLocal 对象集合

在 Thread 类中定义了两个 ThreadLocal.ThreadLocalMap 变量,维护着线程所持有的 ThreadLocal 对象。

public class Thread implements Runnable {
	...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

ThreadLocalMap 是 ThreadLocal 中定义的静态内部类,该 Map 内部的键值对(Map.Entry 对象)中的键是 ThreadLocal 的 弱引用 类型,这样就能保证该 Map 能维护大量的键值对。但因为没有使用引用队列,所以过期的键值对只有在其内部的散列表(Map.Entry 数组)快用尽时,才会被回收 。

static class ThreadLocalMap {
	static class Entry extends WeakReference<ThreadLocal<?>> {
	    /** The value associated with this ThreadLocal. */
	    Object value;
	
	    Entry(ThreadLocal<?> k, Object v) {
	        super(k);
	        value = v;
	    }
	}
	...
	private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);

        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                e.value = value;
                return;
            }

            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

		// 将 新键值对 放到散列表中
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    ...
}        

注意:ThreadLocalMap 中解决散列冲突的方法 nextIndex 的实现为:return ((i + 1 < len) ? i + 1 : 0);,即逐次往散列表后查找空闲位置放入值。

ThreadLocal 自身维护着在 Thread 中的引用

当 ThreadLocal 变量调用 set 方法时,将会主动把自己的引用放入到当前线程的 ThreadLocal 对象集合中。

public void set(T value) {
	// 获取访问 ThreadLocal 对象的当前线程对象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

当线程使用 ThreadLocal 对象设置值时,会首先获取访问该 ThreadLocal 对象的当前线程,然后获取该线程所维护的 ThreadLocal 对象集合,最后把 ThreadLocal 对象,传递给 set 函数的参数 共同组成的键值对,放入到该线程对应的 ThreadLocal 对象集合中,其放入方式如下所示。

// ThreadLocalMap :: set

private void set(ThreadLocal<?> key, Object value) {
	...
	tab[i] = new Entry(key, value);
	...
}

不同线程访问 ThreadLocal 对象时,会使 set 函数的参数 value 以 Entry 的形式在不同的线程中创建一个副本,同一个线程能通过同一个 ThreadLocal 对象访问到同一个副本,这样也就保证了 value 变量的线程局部性,也即 Thread-Loal 变量。

总结一下,通过 ThreadLocal 对象设置的变量最终是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。其存储结构如下图所示:

在这里插入图片描述

内存泄漏问题

内存溢出:要求分配的内存超出了系统实际能给到的量,产生内存溢出错误(OutOfMemeoryError)。

内存泄漏:程序向系统申请的内存不再被使用后不将其归还给系统,导致这块潜在的空闲内存无法分配给其它有需求的程序。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

在 ThreadLocal 不再被外部强引用关联时,JVM 会在下一次垃圾回收时清理其占用的空间,而因为 value 是强引用,此时不会被清理,在 ThreadLocalMap 就会出现 key 为 null 但 value 不为 null 的键值对。

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set、get 方法的时候,会在恰当的时候清理掉 key 为 null 的记录。

  • set 时清除
    在这里插入图片描述
    1. 当散列到的哈希表的位置不为空,指向的 ThreadLocal 实例为空时,说明该位置为一个内存泄漏的位置,直接复用该位置
    2. 当散列到的哈希表的位置为空时,会触发当前位置的后续位置进行“垃圾回收”

    注意:ThreadLocalMap#cleanSomeSlots 方法的注释为:It performs a logarithmic number of scans, as a balance between no scanning (fast but retains garbage) and a number of scans proportional to number of elements, that would find all garbage but would cause some insertions to take O(n) time.

    清除 ThreadLocal 的垃圾对象采用的是启发式算法以做到性能和清除“垃圾”对象的平衡,所以在执行完 ThreadLocalMap#cleanSomeSlots 方法后,ThreadLocalMap 中依然可能存在未被回收的“垃圾”对象

  • get 时清除
    在这里插入图片描述
    可以看出清除“垃圾”对象使用的是启发式算法, ThreadLocal 存在内存泄漏的风险。所以在退出 ThreadLocal 变量的作用域时,应该调用 ThreadLocal#remove() 手动清理 Thread#ThreadLocalMap 中的数据。
try {
    // 业务逻辑,threadLocal#get, threadLocal#set
} finally { 
    threadLocal.remove();
}

另外当 ThreadLocal 和线程池一起使用时,有可能会存在 ThreadLocal 内对象的声明周期比预期的要长,举个例子:

class MyClass {
  private static final ThreadLocal MY_VALUE = new ThreadLocal<>();
  private final Executor executor = Executors.newFixedThreadPool(10);

  void foo() {
    executor.execute(() -> {
      MY_VALUE.set(new MyResource());
      // some logic...
    }); // 预期这个Runnable 块执行完成后,MyResource 对象就会被回收,然而实际并不是……
  }
}

正常情况下,设置 ThreadLocal 变量后,当所在线程执行完毕后,因为线程销毁,所以对应的 ThreadLocal 内的对象也会被销毁(本质它的内部是一个 key 为 Thread WeakHashMap)。然而当使用线程池时,因为线程池会把 Thread 对象池化复用,这时候 ThreadLocal 内的对象声明周期也会拉长,看起来就像内存泄露了一样。这种由于在线程池中使用 ThreadLocal 造成内存泄漏的典型场景是 Java Web 应用:
tomcat 线程池倒置 ThreadLocal 内存泄漏

解决这种内存泄漏的一种可能的优雅做法是:

class MyClass {
  private static final ThreadLocal MY_VALUE = new ThreadLocal<>();
  private final Executor executor = new ThreadPoolExecutor(...老长的构造方法...) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
      super.afterExecute(r, t);
      MY_VALUE.clear(); // 执行完把 ThreadLocal 的值清理掉
    }
  }

  void foo() {
    executor.execute(() -> {
      MY_VALUE.set(new MyResource());
      // some logic...
    });
  }
}

withInitial 工作原理

使用静态方法 withInitial 构造 ThreadLocal 对象时,需要提供一个 Supplier 方法接口实现类来指明 ThreadLocal 对象的初始值,该静态方法的返回值也不是 ThreadLocal 对象本身,而且其子类 SuppliedThreadLocal 的实例。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
   return new SuppliedThreadLocal<>(supplier);
}

SuppliedThreadLocal 则只是重写了父类的 initialValue 方法:父类的该方法只是返回了 null。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

在 ThreadLocal 对象没有调用 set 方法设置值,那么 get 方法将直接返回 Supplier 方法接口实现类提供的初始值,其具体逻辑梳理如下。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 在创建 ThreadLocal 对象,直接调用 get 方法获取值时,该对象的 set 方法也就一次也没有调用过,
    // 就不会将当前 ThreadLocal 对象放入到 Thread 的 ThreadLocal 对象集合中,
    // 所以当前 map = null
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

在创建 ThreadLocal 对象,直接调用 get 方法获取值时,该对象的 set 方法也就一次也没有调用过,就不会将当前 ThreadLocal 对象放入到 Thread 的 ThreadLocal 对象集合中,所以当前的 get 方法的调用逻辑等价于下面的代码

public T get() {
    return setInitialValue();
}

setInitialValue 方法则会调用被 SuppliedThreadLocal 重写的 initialValue 方法,返回由 Supplier 方法接口的实现类提供的初始值

private T setInitialValue() {
	// 调用被 SuppliedThreadLocal 重写的 initialValue 方法,返回由 Supplier 方法接口的实现类提供的初始值
    T value = initialValue();
    
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
	
	// Supplier 方法接口的实现类提供的初始值
    return value;
}

使用规范

阿里巴巴java开发者手册中提到:

【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

即最佳实践应该按照如下方式声明并创建一个 ThreadLocal 对象。

private static final ThreadLocal<Scope> SCOPE_THREAD_LOCAL = new ThreadLocal();

使用 static 修饰 ThreadLocal 对象还有一个好处就是,由于 ThreadLocal 有强引用在,那么在 ThreadLocalMap 里对应的 Entry 的 key 会永远存在,所以执行 remove 操作的时候就可以正确地进行定位并删除。

案例

框架应用

ThreadLocal在Spring中发挥着重要作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块中都出现了它的身影。 想要了解Spring事务管理的底层技术,必须要攻克ThreadLocal。

Spring 事务管理

Spring 使用 ThreadLocal 来设计 TransactionSynchronizationManager 类,实现了事务管理与数据访问服务的解耦,同时也保证了多线程环境下 connection 的线程安全问题。

在 DataSourceTransactionManager 中,doBegin() 开启事务,来看看它是怎么处理connection 资源的。
在这里插入图片描述
首先从数据库连接池中获得一个 connection,并构造一个 connection 包装类,使用这个包装类开启事务,最后通过 TransactionSynchronizationManager 将 connection 与 ThreadLocal 绑定。

TransactionSynchronizationManager 中 bindResource() 的实现
在这里插入图片描述
其中 resources 就是一个 ThreadLocal 变量
在这里插入图片描述
这样就能保证在同一个线程中,多个不同的 DAO 获取到同一个 connection 对象。

在事务提交或回滚时将 connection 与 ThreadLocal 进行解绑。来看看 DataSourceUtils.afterCompletion 方法的实现。
在这里插入图片描述
在这里插入图片描述

奇思妙想

下面的例子展示了如何通过 ThreadLocal 来实现一个线程安全的标识符生成器。

import java.util.concurrent.atomic.AtomicInteger;

public static class ThreadId {
        private static final AtomicInteger NEXT_ID = new AtomicInteger(0);

        private static final ThreadLocal<Integer> THREAD_LOCAL_ID = new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return NEXT_ID.getAndIncrement();
            }
        };

        public static int get() {
            return THREAD_LOCAL_ID.get();
        }
    }

扩展

Netty FastThreadLocal
index 优化

在 JDK 实现中,ThreadLocalMap 使用线性探测的方式解决 hash 冲突的问题,即如果没有找到空闲的 slot,就不断往后尝试,直到找到一个空闲的位置插入 entry,这种方式在经常遇到 hash 冲突时,效率比较低下(无论插入、删除、查找都不是 O(1) 的时间复杂度)。

在这里插入图片描述

FastThreadLocal 直接使用数组避免了 hash 冲突的发生,具体做法是:每个FastThreadLocal 实例创建时,分配一个下标 index;分配 index 使用 AtomicInteger 实现,每个 FastThreadLocal 都能获取到一个不重复的下标。当调用 FastThreadLocal#get() 获取值时,直接从数组获取返回,如 return array[index],如下图:
在这里插入图片描述
其存储结构实现代码为:

static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
static final AtomicInteger nextIndex = new AtomicInteger();
Object[] indexedVariables;

FastThreadLocal 不是所有场景都是 fast 的,当使用 FastThreadLocalThread 才会快,如果使用普通线程反而比 ThreadLocal 还慢!其正确使用姿势为:

public class FastThreadLocalTest {

    private static final FastThreadLocal<Integer> FAST_THREAD_LOCAL = new FastThreadLocal<>();

    public static void main(String[] args) {
        Runnable runnable1 = () -> {
            for (int i = 0; i < 10; i++) {
                FAST_THREAD_LOCAL.set(i);
                System.out.println(Thread.currentThread().getName() + "#set = " + FAST_THREAD_LOCAL.get());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        new FastThreadLocalThread(runnable1, "fastThread1").start();
      }
  }

从示例代码中可以看出 FastThreadLocal 没有像 ThreadLocal 那样使用 try...finally 对ThreadLocal 最后执行 remove 操作。原因在构建 FastThreadLocalThread 的时候,对传入的 Runnable 进行了包装,包装成了 FastThreadLocalRunnable。

public FastThreadLocalThread(Runnable target, String name) {
    // 对Runnable进行包装
    super(FastThreadLocalRunnable.wrap(target), name);
    this.cleanupFastThreadLocals = true;
}

FastThreadLocalRunnable 在执行完之后默认会调用 FastThreadLocal#removeAll,无需自己调用,对使用者更友好。

final class FastThreadLocalRunnable implements Runnable {
    private final Runnable runnable;

    private FastThreadLocalRunnable(Runnable runnable) {
        this.runnable = (Runnable)ObjectUtil.checkNotNull(runnable, "runnable");
    }

    public void run() {
        try {
            this.runnable.run();
        } finally {
            // 执行完runnable,调用removeAll
            FastThreadLocal.removeAll();
        }
    }

    static Runnable wrap(Runnable runnable) {
        return (Runnable)(runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable));
    }
}
字节填充解决伪共享问题
缓存常识

下图是CPU三级缓存结构。L1、L2、L3分别表示一级缓存、二级缓存、三级缓存,越靠近CPU的缓存,速度越快,容量也越小。所以L1缓存很小但很快,并且紧靠着在使用它的CPU内核;L2大一些,也慢一些,并且仍然只能被一个单独的CPU核使用;L3更大、更慢,并且被单个插槽上的所有CPU核共享;最后是主存,由全部插槽上的所有CPU核共享。
在这里插入图片描述
当CPU执行运算的时候,它先去L1查找所需的数据、再去L2、然后是L3,如果最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要尽量确保数据在L1缓存中。另外,线程之间共享一份数据的时候,需要一个线程把数据写回主存,而另一个线程访问主存中相应的数据。下面是从CPU访问不同层级数据的时间概念:
在这里插入图片描述
可见CPU读取主存中的数据会比从L1中读取慢了近2个数量级。

缓存行

Cache 是由很多个 CacheLine 组成的,每个 CacheLine 通常是 64 字节,并且它有效地引用主内存中的一块地址。一个 Java 的 long 类型变量是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。CPU 缓存的最小单位是 Cacheline,CPU 每次从主存中拉取数据时,会把相邻的数据也存入同一个 CacheLine。在访问一个 long 数组的时候,如果数组中的一个值被加载到缓存中,它会自动加载另外 7 个。因此你能非常快的遍历这个数组。事实上,你可以非常快速的遍历在连续内存块中分配的任意数据结构。

下面代码是测试利用CacheLine的特性和不利用CacheLine的特性的效果对比。

public class CPUCacheLineTest {

    public static final long[][] ARRAY = new long[1024 * 1024][];

    public static void main(String[] args) {
        for (int i = 0; i < 1024 * 1024; i++) {
            ARRAY[i] = new long[8];
            for (int j = 0; j < 8; j++) {
                ARRAY[i][j] = 0L;
            }
        }
        long value = 0L;
        StopWatch stopWatch  = getWatcher();
        for (int i = 0; i < 1024 * 1024; i += 1) {
            for (int j = 0; j < 8; j++) {
                value = ARRAY[i][j];
            }
        }
        System.out.println("使用CacheLine特性,cost:" + stopWatch.getElapsedTime() + "毫秒");

        //---------------------分割线--------------------
        stopWatch  = getWatcher();
        for (int i = 0; i < 8; i += 1) {
            for (int j = 0; j < 1024 * 1024; j++) {
                value = ARRAY[j][i];
            }
        }
        System.out.println("不使用CacheLine特性,cost:" + stopWatch.getElapsedTime() + "毫秒");
    }
}

在这里插入图片描述

伪共享

由于多个线程同时操作同一 CacheLine 中的不同变量,但是这些变量之间却没有啥关联,但每次修改,都会导致缓存的数据变成无效,从而明明没有任何修改的内容,还是需要去主存中读,由于 CPU 缓存的最小单位是一个 CacheLine,这种现象就是伪共享。

如果让多线程频繁操作的并且没有关系的变量分布在不同的 CacheLine 中,那么就不会因为 CacheLine 失效而影响其他没有修改的变量去读主存,这样性能就会好很多。

FastThreadLocal InternalThreadLocalMap使用字节填充的代码如下:

// Cache line padding (must be public)
// With CompressedOops enabled, an instance of this class should occupy at least 128 bytes.
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8, rp9;

使用JOL工具查看InternalThreadLocalMap对象占用的字节数:

public class JOLTest {

    public static void main(String[] args) throws Exception {
        System.out.println(ClassLayout.parseClass(InternalThreadLocalMap.class).toPrintable());
    }
}
io.netty.util.internal.InternalThreadLocalMap object internals:
OFFSET  SIZE                                       TYPE DESCRIPTION                                                    VALUE
0      12                                            (object header)                                                N/A
12     4                                        int UnpaddedInternalThreadLocalMap.futureListenerStackDepth        N/A
16     4                                        int UnpaddedInternalThreadLocalMap.localChannelReaderStackDepth    N/A
20     4                         java.lang.Object[] UnpaddedInternalThreadLocalMap.indexedVariables                N/A
24     4                              java.util.Map UnpaddedInternalThreadLocalMap.handlerSharableCache            N/A
28     4       io.netty.util.internal.IntegerHolder UnpaddedInternalThreadLocalMap.counterHashCode                 N/A
32     4   io.netty.util.internal.ThreadLocalRandom UnpaddedInternalThreadLocalMap.random                          N/A
36     4                              java.util.Map UnpaddedInternalThreadLocalMap.typeParameterMatcherGetCache    N/A
40     4                              java.util.Map UnpaddedInternalThreadLocalMap.typeParameterMatcherFindCache   N/A
44     4                    java.lang.StringBuilder UnpaddedInternalThreadLocalMap.stringBuilder                   N/A
48     4                              java.util.Map UnpaddedInternalThreadLocalMap.charsetEncoderCache             N/A
52     4                              java.util.Map UnpaddedInternalThreadLocalMap.charsetDecoderCache             N/A
56     4                        java.util.ArrayList UnpaddedInternalThreadLocalMap.arrayList                       N/A
60     4                           java.util.BitSet InternalThreadLocalMap.cleanerFlags                            N/A
64     8                                       long InternalThreadLocalMap.rp1                                     N/A
72     8                                       long InternalThreadLocalMap.rp2                                     N/A
80     8                                       long InternalThreadLocalMap.rp3                                     N/A
88     8                                       long InternalThreadLocalMap.rp4                                     N/A
96     8                                       long InternalThreadLocalMap.rp5                                     N/A
104    8                                       long InternalThreadLocalMap.rp6                                     N/A
112    8                                       long InternalThreadLocalMap.rp7                                     N/A
120    8                                       long InternalThreadLocalMap.rp8                                     N/A
128    8                                       long InternalThreadLocalMap.rp9                                     N/A
Instance size: 136 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

疑问:为什么要填充 9 个 long 使 instance 的 size 等于 136Bytes 呢?为什么只在InternalThreadLocalMap中属性的后面一侧增加字节填充呢?github上面有一些讨论,这个回答可能解释了填充9个long的原因:

I have checked the code in old version and found that the size of InternalThreadLocalMap is 128Bytes in version 4.0.33. And now in latest code in github the size of InternalThreadLocalMap is 136. And the reason is that some has added two parameters: cleanerFlags (in class InternalThreadLocalMap) and arrayList (in parent class UnpaddedInternalThreadLocalMap).

In my view, the contributors has pushed the two parameters ignoring the Cache line padding. So it is a problem!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值