面试必备之ThreadLocal

勤奋不是嘴上说说而已,而是实际的行动,在勤奋的苦度中持之以恒,永不退却。业精于勤,荒于嬉;行成于思,毁于随。在人生的仕途上,我们毫不迟疑地选择勤奋,她是几乎于世界上一切成就的催产婆。只要我们拥着勤奋去思考,拥着勤奋的手去耕耘,用抱勤奋的心去对待工作,浪迹红尘而坚韧不拔,那么,我们的生命就会绽放火花,让人生的时光更加的闪亮而精彩。

导读:本篇文章讲解 面试必备之ThreadLocal,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

概述

通常情况下,一个类的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。

采用空间换时间,用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭

ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本(局部变量)。

实例

public class ThreadLocalTest {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        }
    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

代码非常简单,启动两个线程,分别设置一个小于100的随机数,然后取出来。

分析

ThreadLocal是一个泛型类,重要属性包括:

// 当前 ThreadLocal的hashCode,由 nextHashCode() 计算而来,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔数,主要与斐波那契散列法以及黄金分割有关
private static final int HASH_INCREMENT = 0x61c88647;
// 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 保证在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();

其提供的主要方法如下:

// 获取值,如果没有返回 null
public T get()
public void set(T)
// 提供初始值,当调用 get 方法时如果之前没有设置过则会调用该方法获取初始值,默认为 null
protected T initialValue()
// 删掉当前线程对应的值,如果删掉后再次调用get,则会再调用initialValue获取初值
public void remove()

其中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);
}

ThreadLocal类中的getMap方法

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// Thread类中声明的threadLocals变量
ThreadLocal.ThreadLocalMap threadLocals = null;

其中,ThreadLocalMap
每个线程维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。有ThreadLocalMap的内部类,该类为一个采用线性探测法实现的HashMap。它的key为ThreadLocal对象而且还使用WeakReference,ThreadLocalMap正是用来存储变量副本的。

ThreadLocal是一个为线程提供线程局部变量的工具类。为线程提供一个线程私有的变量副本,这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。

问题
ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用,不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。ThreadLocal的get、set和remove方法都实现对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。

解决方法:

  1. 在使用完后,没有remove
  2. private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉

其中 static 是为了确保全局只有一个保存 String 对象的 ThreadLocal 实例;final 确保 ThreadLocal 的实例不可更改,防止被意外改变,导致放入的值和取出来的不一致,另外还能防止 ThreadLocal 的内存泄漏。

用途

  1. 实现单个线程单例以及单个线程上下文信息存储。ThreadLocal在Spring中处处可见:管理Request作用域中的Bean、事务管理、任务调度、AOP等模块。Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作。又,常用于用户登录控制,如记录session信息。
  2. 非线程安全的对象使用ThreadLocal之后就会变得线程安全。如SimpleDateFormat,SDF,在多线程并发的情况下是线程不安全的,可能会抛出NumberFormatException或其它异常。使用ThreadLocal包装,直接创建一个共享实例对象,每个线程都有自己的SDF实例对象。
// SDF非线程安全的原因:CalendarBuilder类的establish方法部分代码
Calendar establish(Calendar cal) {
    // 省略部分不重要代码
    cal.clear();
    // 省略部分不重要代码
    return cal;
}
  1. 承载一些线程相关的数据,避免在方法中来回传递参数

ThreadLocal vs Synchronized

都能实现多线程环境下的共享变量的线程安全,区别呢?

Synchronized通过锁(同步)来实现内存共享,ThreadLocal为每个线程维护一个本地变量,即通过避免对象的共享来实现。锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。

同步机制是为了同步多线程对相同资源的并发访问,是为了线程间数据共享的问题,而 ThreadLocal 是隔离多线程数据共享,从根本上就不在多个线程之间共享资源,这样自然就不需要多线程的同步机制。

拓展

基于ThreadLocal思想的拓展类有很多。并且在其他语言里面也有这种思想的实现。

线程局部存储

ThreadLocal底层的技术。TLS,thread local storage。全局变量与函数内定义的静态变量,是各个线程都可以访问的共享变量。

线程局部存储-百科
线程局部存储TLS

线程局部握手

Thread-Local Handshakes,JDK 10引入的技术,也叫线程本地握手,不执行全局VM安全点也能对线程执行回调,同时实现单线程停止回调。利用所谓的非对称Dekker同步技术,通过与Java线程握手来消除一些内存障碍(memory barrier)。不再依赖内存屏障实现同步,直接使用Dekker算法。

Handshakes和普通的safepoint机制有所不同:

A handshake operation is a callback that is executed for each JavaThread while that thread is in a safepoint safe state. The callback is executed either by the thread itself or by the VM thread while keeping the thread in a blocked state. The big difference between safepointing and handshaking is that the per thread operation will be performed on all threads as soon as possible and they will continue to execute as soon as it’s own operation is completed. If a JavaThread is known to be running, then a handshake can be performed with that single JavaThread as well.

In the initial implementation there will be a limitation of at most one handshake operation in flight at a given time. The operation can, however, involve any subset of all JavaThreads. The VM thread will coordinate the handshake operation through a VM operation which will in effect prevent global safepoints from occurring during the handshake operation.

The current safepointing scheme is modified to perform an indirection through a per-thread pointer which will allow a single thread’s execution to be forced to trap on the guard page. Essentially, at all times there will be two polling pages: One which is always guarded, and one which is always unguarded. In order to force a thread to yield, the VM updates the per-thread pointer for the corresponding thread to point to the guarded page.

Thread-local handshakes will be implemented initially on x64 and SPARC. Other platforms will fall back to normal safepoints. A new product option, -XX:ThreadLocalHandshakes (default value true), allows users to select normal safepoints on supported platforms.

ThreadLocalRandom

待学习

InheritableThreadLocal

在子线程中获取主线程threadLocal中set方法设置的值,如何实现?
使用InheritableThreadLocal。通过ThreadLocal threadLocal = new InheritableThreadLocal(),在子线程中就可以通过get方法获取到主线程set方法设置的值。
InheritableThreadLocal继承ThreadLocal重写childValuegetMapcreateMap方法:

/**
 * 在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
 */
protected T childValue(T parentValue) {
    return parentValue;
}
/**
 * 重写getMap,操作InheritableThreadLocal时,将只影响Thread类中的inheritableThreadLocals变量,与threadLocals变量不再有关系
 */
ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
}
/**
 * 类似于getMap,操作InheritableThreadLocal时,将只影响Thread类中的inheritableThreadLocals变量,
 * 与threadLocals变量不再有关系
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

当使用InheritableThreadLocal创建实例对象时,当前线程Thread对象中维护一个inheritableThreadLocals变量,也是ThreadLocalMap类型,在创建子线程的过程中,将主线程维护的inheritableThreadLocals变量的值复制到子线程维护的inheritableThreadLocals变量中,这样子线程就可以获取到主线程设置的值。

应用
调用链追踪:在调用链系统设计中,为了优化系统运行速度,会使用多线程编程,为了保证调用链ID能够自然的在多线程间传递,需要考虑ThreadLocal传递问题。

TransmittableThreadLocal

阿里开源,GitHub

TransmittableThreadLocal详解
增强版的ThreadLocal-TransmittableThreadLocal

FastThreadLocal

源自Netty,
Netty中FastThreadLocal源码分析

参考

正确理解Thread Local的原理与适用场景
深入研究java.lang.ThreadLocal类
一文搞懂 ThreadLocal 原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/142292.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!