Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >浅谈ThreadLocal

浅谈ThreadLocal

作者头像
程序猿杜小头
发布于 2022-12-01 13:39:43
发布于 2022-12-01 13:39:43
45300
代码可运行
举报
文章被收录于专栏:程序猿杜小头程序猿杜小头
运行总次数:0
代码可运行

ThreadLocal因为内存泄漏问题早已在江湖中声名远扬,引得一众开发人员的吐槽。于是,ThreadLocal 的设计者之一Josh Bloch不得不出来辟谣:ThreadLocal的设计毫无问题,而且历经数次优化后其性能越来越好,内存泄漏是由开发者误用造成的,我们不背这个锅!由此可见,ThreadLocal 是有一定上手门槛的,希望大家在读完本文后可以正确地使用它。

1. 认识ThreadLocal

ThreadLocal用于保存只能由同一线程进行读取、更新和删除操作的线程本地变量,换句话说,该变量对于不同线程是透明的。线程本地变量由ThreadLocalMap承载,ThreadLocalMap 内部结构为key/value键值对,key对应 ThreadLocal 自身,value对应线程本地变量;get()set()remove()方法最终操作的目标就是 ThreadLocalMap。线程本地变量对于不同线程是透明的,是如何保证的呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

原来每个线程都持有一个 ThreadLocalMap 实例变量,看到这里,大家也许意识到了,ThreadLocal只是操作线程中ThreadLocalMap这一实例变量的入口罢了!


如果隐去实现细节,ThreadLocal 的源码结构一目了然:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocal<T> {
    protected T initialValue() {}
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
    public T get() {}
    public void set(T value) {}
    public void remove() {}
    static class ThreadLocalMap {}
}

initialValue()withInitial()方法用于初始化 ThreadLocal,即为线程本地变量设定初始值,否则线程本地变量的初始值默认为null;initialValue() 与 withInitial() 方法将会在当前线程通过调用 get() 方法获取本地变量时进行初始赋值操作。具体细节如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T) e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            map.set(this, value);
        } else {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
        return value;
    }
}

ThreadLocal 的初始化要格外注意,下面这段代码就是典型的、错误的初始化方式,甚至一位腾讯大佬的博客上也是这么初始化的,大家有没有发现啥问题?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class BadInitializationWithThreadLocal {
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<>();
    
    static {
        DATE_FORMAT.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    public String formatDate(Date date) {
        SimpleDateFormat simpleDateFormat = DATE_FORMAT.get();
        return simpleDateFormat.format(date);
    }
}

上述初始化方式的问题在于静态初始化代码块。先来回顾下类加载的相关知识,初始化是类加载过程的最后一个阶段,初始化阶段就是执行<clinit>()方法的过程,<clinit>()方法并不是开发人员在Java代码中直接编写的方法,而是由编译器自动收集类中静态变量的赋值语句和静态初始化代码块合并而产生的;然而一个类一般只会被同一类加载器加载一次,那静态初始化代码块自然只有一次执行机会了。试想一下,如果在线程A中触发了BadInitializationWithThreadLocal的加载,那么其他线程执行其formatDate()方法一定会抛出NullPointerException

set()方法旨在为当前线程建立关于 ThreadLocal 与线程本地变量的映射关系;而remove()方法恰恰相反,它会通过将key/value均置为null来删除这种映射关系;具体源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            map.set(this, value);
        } else {
            t.threadLocals = new ThreadLocalMap(this, value);
        }
    }
    
    public void remove() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            map.remove(this);
        }
    }
}

关于 ThreadLocalMap,这里只提两点:1) 哈希表基于数组实现;2) 哈希表基于线性探测解决哈希冲突问题。更多实现细节,请大家自行阅读源码。

2. 应用场景

ThreadLocal 的应用场景虽然五花八门,但普遍可以归为以下两个场景中的一个。

2.1 保存非线程安全对象,避免多线程并发调用

在多线程环境中,对线程不安全的共享实例变量的访问,一般需要对该共享实例变量加锁。但 ThreadLocal 提供了另一种思路,那就是将共享实例变量变为线程独有的实例变量,各个线程访问的都是各自副本,自然也就不存在竞争关系了。

阿里巴巴<Java开发手册>中提到SimpleDateFormat是线程不安全的,要避免多线程访问,如下图所示:

下面就来模拟一下多线程访问 SimpleDateFormat 究竟会有什么问题。为了进一步提升多线程环境下的并发竞争度,这里使用了j.u.c包中的CyclicBarrier,在20个线程并发访问 SimpleDateFormat 的format()方法前调用 CyclicBarrier 的await()方法,以确保这20个线程准备就绪

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalMain1 {
    public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 构建线程池
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("exe-pool-%d").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                20,
                50,
                0L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(128),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        // 线程安全的SET
        Set<String> dateSet = Sets.newCopyOnWriteArraySet();
        // J.U.C包中两大利器:递减锁存器与循环屏障
        CountDownLatch countDownLatch = new CountDownLatch(20);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(20);

        for (int i = 0; i < 20; i++) {
            final int offset = i;
            threadPoolExecutor.submit(() -> {
                try {
                    // 等待开闸放水,主要是为了提升多线程环境下的并发竞争度,阻塞性方法
                    cyclicBarrier.await();
                    Calendar calendar = Calendar.getInstance();
                    calendar.add(Calendar.DATE, offset);
                    String date = simpleDateFormat.format(calendar.getTime());
                    dateSet.add(date);
                    // 递减锁存器递减一,非阻塞性方法
                    countDownLatch.countDown();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        // 主线程等待所有线程执行结束,阻塞性方法
        countDownLatch.await();
        // 打印结果
        System.out.println("总数 = " + dateSet.size());
        System.out.println("==========");
        System.out.println(dateSet.stream().sorted().collect(Collectors.joining("\r\n")));
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

运行结果

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
总数 = 5
==========
2021-09-30 20:53:48
2021-10-01 20:53:48
2021-10-06 20:53:48
2021-10-07 20:53:48
2021-10-30 20:53:48

从上述运行结果来看,控制台仅仅打印了五个日期值,显然是错误的。这主要因为 SimpleDateFormat 的format()方法涉及Calendar.setTime(date)操作,而该Calendar实例是一个实例/全局变量;在多线程环境下,对单个 SimpleDateFormat 实例内的全局变量进行访问肯定是有风险的。

解决方案有很多,比如加锁、使用DateUtils工具类、使用Java 8新增的DateTimeFormatter等。这里我们使用 ThreadLocal 来试试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalMain2 {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(
                () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 构建线程池
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("exe-pool-%d").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                20,
                50,
                0L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(128),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        // 线程安全的SET
        Set<String> dateSet = Sets.newCopyOnWriteArraySet();
        // J.U.C包中两大利器:递减锁存器与循环屏障
        CountDownLatch countDownLatch = new CountDownLatch(20);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(20);

        for (int i = 0; i < 20; i++) {
            final int offset = i;
            threadPoolExecutor.submit(() -> {
                try {
                    // 等待开闸放水,主要是为了提升多线程环境下的并发竞争度,阻塞性方法
                    cyclicBarrier.await();
                    Calendar calendar = Calendar.getInstance();
                    calendar.add(Calendar.DATE, offset);
                    String date = simpleDateFormatThreadLocal.get().format(calendar.getTime());
                    dateSet.add(date);
                    // 递减锁存器递减一,非阻塞性方法
                    countDownLatch.countDown();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                } finally {
                    simpleDateFormatThreadLocal.remove();
                }
            });
        }
        // 主线程等待所有线程执行结束,阻塞性方法
        countDownLatch.await();
        // 打印结果
        System.out.println("总数 = " + dateSet.size());
        System.out.println("==========");
        System.out.println(dateSet.stream().sorted().collect(Collectors.joining("\r\n")));
        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

运行结果

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
总数 = 20
==========
2021-09-28 20:54:30
2021-09-29 20:54:30
2021-09-30 20:54:30
2021-10-01 20:54:30
2021-10-02 20:54:30
2021-10-03 20:54:30
2021-10-04 20:54:30
2021-10-05 20:54:30
2021-10-06 20:54:30
2021-10-07 20:54:30
2021-10-08 20:54:30
2021-10-09 20:54:30
2021-10-10 20:54:30
2021-10-11 20:54:30
2021-10-12 20:54:30
2021-10-13 20:54:30
2021-10-14 20:54:30
2021-10-15 20:54:30
2021-10-16 20:54:30
2021-10-17 20:54:30

恭喜,运行结果符合预期!

2.2 保存线程上下文对象,避免多层级参数透传

Spring Security用于为Spring应用提供 认证 (authentication) 与 授权 (authorization) 服务,在其SecurityFilterChain过滤器链中默认有15个过滤器;其中SecurityContextPersistenceFilter处于整个过滤器链的顶端,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[0 ] WebAsyncManagerIntegrationFilter
[1 ] SecurityContextPersistenceFilter
[2 ] HeaderWriterFilter
[3 ] CsrfFilter
[4 ] LogoutFilter
[5 ] UsernamePasswordAuthenticationFilter
[6 ] DefaultLoginPageGeneratingFilter
[7 ] DefaultLogoutPageGeneratingFilter
[8 ] BasicAuthenticationFilter
[9 ] RequestCacheAwareFilter
[10] SecurityContextHolderAwareRequestFilter
[11] AnonymousAuthenticationFilter
[12] SessionManagementFilter
[13] ExceptionTranslationFilter
[14] FilterSecurityInterceptor

既然聊到SecurityContextPersistenceFilter,就不得不提SecurityContext,其可以用于存储当前用户、当前用户是否已认证、当前用户具有哪些权限等信息;而 SecurityContext 则通过SecurityContextHolder中的 ThreadLocal 来维护,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SecurityContextHolder {
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

    public static void clearContext() {
        contextHolder.remove();
    }

    public static SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();
        if (ctx == null) {
            ctx = new SecurityContextImpl();
            contextHolder.set(ctx);
        }
        return ctx;
    }

    public static void setContext(SecurityContext context) {
        contextHolder.set(context);
    }
}

在 SecurityFilterChain 的过滤器链中,存在若干过滤器需要借助 SecurityContext 来存取当前用户的相关信息的场景,那该如何在不修改doFilter()方法签名的前提下实现多过滤器层级下SecurityContext的透传呢?既然一个Http请求的完整执行流程是在一个线程上下文中完成的,那显然可以通过ThreadLocal来实现,SecurityContextPersistenceFilter 就是这么干的哈。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        SecurityContext contextBeforeChainExecution = this.securityContextRepository.loadContext(holder);
        try {
            // 设置当前线程上下文中的SecurityContext实例
            // 一般是一个空的SecurityContext实例
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            // 执行过滤器链中的后续过滤器
            chain.doFilter(holder.getRequest(), holder.getResponse());
        } finally {
            // SecurityContextPersistenceFilter后续的过滤器执行完毕后
            // 清空SecurityContext实例
            SecurityContextHolder.clearContext();
        }
    }
}

3. 内存泄漏 (Memory Leak)

3.1 何为内存泄露

如果一个对象已经没有什么用途,但因为有一个有效的引用(reference) 指向它而导致垃圾收集器无法销毁该对象,那就可以说该对象造成了内存泄漏 。内存泄露并不是一个好兆头,持续不断地发生内存泄漏最终会耗尽内存资源,Java应用程序也最终会因java.lang.OutOfMemoryError异常终止。来看一个内存泄露的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MemoryLeakMain {
    public static void main(String[] args) {
        // 模拟占用50MB堆内存
        byte[] byteArr = new byte[50 * 1024 * 1024];
        // 通过死循环模拟长生命周期线程
        for (;;) {}
    }
}

上述代码表明:在一长生命周期的主线程中,定义了一个长度为52428800且毫无用处的字节数组,约占用50MB堆内存空间。运行上述程序,然后通过Eclipse Memory Analyzer工具进行内存泄露探测,其结果如下:

上图表明探测到一个内存泄露可疑点,详细描述信息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
The thread java.lang.Thread @ 0x607e06430 main keeps local variables with total size 52,430,064 bytes.
The memory is accumulated in one instance of “byte[], loaded by “<system class loader>, which occupies 52,428,816  bytes.

Keywords
  byte[]
  com.example.crimson_typhoon.threadlocal.MemoryLeakMain.main([Ljava/lang/String;)V
  MemoryLeakMain.java:9

下面就来验证下这个字节数组对象究竟有没有造成内存泄漏。通过VisualVM手动进行垃圾回收,但堆内存占用空间仅少量收缩,依然保持60MB左右的体量,这说明垃圾收集器压根就没销毁这个没卵用的大对象,如下图所示:

那垃圾收集器为什么没有销毁该字节数组对象呢?因为垃圾收集器从GC Root根节点向下搜索发现:该字节数组对象与GC Root存在一条结结实实的引用链 (reference chain),或者用图论的行话说就是从GC Root到这个字节数组对象是可达的,所以没有销毁这个字节数组对象,这里扮演GC Root角色的正是主线程。

3.2 ThreadLocal与内存泄漏

概念搞明白了,下面就要分析 ThreadLocal 为什么会造成内存泄漏问题了。阅读源码发现:ThreadLocalMap 内部封装了一key/value结构的内部静态类Entry,其继承自WeakReference<ThreadLocal<?>>,表明 Entry 的 key 由弱引用关联,而 value 则是强引用,所以关于 ThreadLocal 引发内存泄露的话题主要就是围绕 Entry 的 value 展开的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocal {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

弱引用 弱引用可以描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象若不存在强引用则只能生存到下一次垃圾收集发生为止;当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2后提供了WeakReference类来实现弱引用。

下面贴了一段代码,大家觉得这个大字节数组对象会造成内存泄漏吗?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalMemoryLeakMain {
    public static void main(String[] args) {
        // 模拟占用100MB堆内存
        byte[] byteArr = new byte[100 * 1024 * 1024];

        ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
        threadLocal.set(byteArr);
        threadLocal = null;
        byteArr = null;

        // 通过死循环模拟长生命周期线程
        for (;;) {}
    }
}

继续借助Eclipse Memory Analyzer工具进行内存泄露探测,其结果如下:

卧槽,从上图来看又检测到一个内存泄露可疑点,详细描述信息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
One instance of “java.lang.ThreadLocal$ThreadLocalMap$Entry” loaded by “<system class loader>” occupies 104,857,648  bytes. The instance is referenced by java.lang.Thread @ 0x60b606430 main , loaded by “<system class loader>. 

The thread java.lang.Thread @ 0x60b606430 main keeps local variables with total size 1,264 bytes.
The memory is accumulated in one instance of “byte[], loaded by “<system class loader>, which occupies 104,857,616  bytes.

Keywords
  java.lang.ThreadLocal$ThreadLocalMap$Entry
  byte[]

继续通过VisualVM手动触发垃圾回收后来观察堆内存是否有一个明显的收缩趋势,从下图来看收缩效果微乎其微啊:

至于垃圾收集器没有销毁该字节数组对象的原因就不再赘述了,直接上图:


下面聊一聊内存泄露的场景。上述代码中通过死循环来模拟一长生命周期的线程,如果没有这个死循环,ThreadLocal 还会引起内存泄露吗?答案是不会,因为主线程紧接着就执行完毕了,线程都死亡了,那它引用的 ThreadlocalMap 对象肯定也被回收掉了啊··· 事实上,ThreadLocal 内存泄露现象一般多发于长生命周期的线程中,有哪些线程的生命周期比较长呢?大家肯定猜到了:线程池中的核心 (core) 线程。

那如何才能使得垃圾收集器回收该对象呢?很简单,追加threadLocal.remove();这一行代码即可,效果立竿见影:

3.3 Entry中的key为什么被设计成弱引用

这个问题反过来问可能更容易得出答案:如果 Entry 中的 key 被设计成强引用,会出现什么问题呢?还是通过代码来说明:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class WeakReferenceEntryKeyMain {
    public static void main(String[] args) {
        byte[] byteArr = new byte[100 * 1024 * 1024];

        ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
        threadLocal.set(byteArr);
        threadLocal = null;

        for (;;) { }
    }
}

运行上述代码,然后通过Eclipse Memory Analyzer可以很直观地观测到:ThreadLocal 对象被垃圾收集器销毁了,有图为证:

假如 Entry 中的 key 被设计为强引用,即使threadLocal变量被置为null也无法使得垃圾收集器销毁这个 ThreadLocal 对象。所以,最终的结论就是:将Entry中的key设计成弱引用就是为了不干扰用户主动销毁 ThreadLocal 对象的意图。

4. 总结

最后再强调一下ThreadLocal的使用场景:

  1. 保存非线程安全对象,避免多线程并发调用;
  2. 保存线程上下文对象,避免多层级参数透传。

另外,尽量将set()remove()这俩方法搭配起来使用,尤其是在线程池中,一定要使用使用remove()方法,切莫当归还线程对象时,还将线程本地变量驻留在线程对象中!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿杜小头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
ThreadLocal企业中真实应用
SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String、Date等等,都是交友Calendar引用来储存的,这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,parse方法里没有保证原子性,所以存在线程安全问题:
公众号 IT老哥
2020/09/16
1.2K0
ThreadLocal企业中真实应用
Java并发-ThreadLocal
每个线程内部都有一个ThreadLocalMap,每个ThreadLocalMap里面都有一个Entry[]数组,Entry对象由ThreadLocal和数据组成。
lpe234
2021/03/02
4210
ThreadLocal 你真的用不上吗?
点击上方“芋道源码”,选择“设为星标” 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java 2021 超神之路,很肝~ 中文详细注释的开源项目 RPC 框架 Dubbo 源码解析 网络应用框架 Netty 源码解析 消息中间件 RocketMQ 源码解析 数据库中间件 Sharding-JDBC 和 MyCAT 源码解析 作业调度中间件 Elastic-Job 源码解析 分布式事务中间件 TCC-Transaction
芋道源码
2022/09/19
2700
ThreadLocal 你真的用不上吗?
ThreadLocal 的原理及问题,一网打尽!
ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获取到数据。
田维常
2023/11/30
2500
ThreadLocal 的原理及问题,一网打尽!
来讲讲你对ThreadLocal的理解
面试的时候被问到ThreadLocal的相关知识,没有回答好(奶奶的,现在感觉问啥都能被问倒),所以我决定先解决这几次面试中都遇到的高频问题,把这几个硬骨头都能理解的透彻的说出来了,感觉最起码不能总是一轮游。
纪莫
2020/09/11
3370
揭秘ThreadLocal
ThreadLocal是开发中最常用的技术之一,也是面试重要的考点。本文将由浅入深,介绍ThreadLocal的使用方式、实现原理、内存泄漏问题以及使用场景。 ThreadLocal作用 在并发编程中时常有这样一种需求:每条线程都需要存取一个同名变量,但每条线程中该变量的值均不相同。 如果是你,该如何实现上述功能?常规的思路如下: 使用一个线程共享的Map<Thread,Object>,Map中的key为线程对象,value即为需要存储的值。那么,我们只需要通过map.get(Thread.curre
大闲人柴毛毛
2018/03/29
1.1K0
揭秘ThreadLocal
平时常说的ThreadLocal,今天就彻底解决它
ThreadLocal是多线程处理中非常重要的一个工具,比如数据库连接池存放Connection、存放本地参数等作用,面试也经常会问到它的应用及原理,本文就将从外到内地学习一下ThreadLocal。
beifengtz
2019/07/23
2.3K2
ThreadLocal的应用场景和注意事项有哪些?
最近一个小伙伴把项目中封装的日期工具类用在多线程环境下居然出了问题,来看看怎么回事吧
Java识堂
2020/04/22
1K0
ThreadLocal的应用场景和注意事项有哪些?
探索JAVA并发 - ThreadLocal
SimpleDateFormat是我们常用的日期格式化工具,但熟悉的朋友都知道它是线程不安全的。
acupt
2019/08/26
4000
ThreadLocal 原理
从上面可以看出,Thread 类中有一个 threadLocals 和 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下,这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set 或 get 方法时才创建它们,实际上调用这两个方法的时候,真正调用的是 ThreadLocalMap 类对应的 get()、set()方法。
happyJared
2019/07/09
2820
ThreadLocal原理知多少
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?JDK 中提供的ThreadLocal类正是为了解决这样的问题。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
黑洞代码
2021/03/19
3850
服了,一个ThreadLocal被问出了花
地铁上,小帅无力地倚靠着杆子,脑子里尽是刚才面试官的夺命连环问,“用过ThreadLocal么?ThreadLocal是如何解决共享变量访问的安全性的呢?你觉得啥场景下会用到ThreadLocal? 我们在日常用ThreadLocal的时候需要注意什么?ThreadLocal在高并发场景下会造成内存泄漏吗?为什么?如何避免?......”
程序员老猫
2024/02/22
1610
服了,一个ThreadLocal被问出了花
浅谈ThreadLocal
ThreadLocal顾名思义,线程本地,即各个线程互不干扰的空间,每个线程只能看到当前线程放入的对象。
你的益达
2020/08/14
2780
说一说线程局部变量ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
简单的程序员
2020/04/21
7990
说一说线程局部变量ThreadLocal
ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
崔笑颜
2020/06/08
4290
ThreadLocal的八个关键知识点
无论是工作还是面试中,我们都会跟ThreadLocal打交道,今天就跟大家聊聊ThreadLocal的八个关键知识点哈~
捡田螺的小男孩
2023/02/24
3240
ThreadLocal的八个关键知识点
ThreadLocal的使用及原理分析
ThreadLocal称作线程本地存储。简单来说,就是ThreadLocal为共享变量在每个线程中都创建一个副本,每个线程可以访问自己内部的副本变量。这样做的好处是可以保证共享变量在多线程环境下访问的线程安全性。
日薪月亿
2019/05/14
5620
ThreadLocal的使用及原理分析
ThreadLocal以及内存泄漏
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用ThreadLocal,就可能会导致内存泄漏。
猿芯
2021/05/27
8940
ThreadLocal以及内存泄漏
ThreadLocal的原理解析以及应用场景分析
JDK对ThreadLocal的定义如下:TheadLocal提供了线程内部的局部变量:每个线程都有自己的独立的副本;ThreadLocal实例通常是类中的private static字段,该类一般与线程状态相关(或线程上下文)中使用。只要线程处于活动状态且ThreadLocal实例时可访问的状态下,每个线程都持有对其线程局部变量的副本的隐式引用,在线程消亡后,ThreadLocal实例的所有副本都将进行垃圾回收。
码农飞哥
2021/08/18
2700
重温并发知识,从ThreadLocal开始
ThreadLocal提供线程的局部变量,这种变量与普通变量的区别在于,每个访问这种变量的线程都有自己的、独立的变量副本。用于解决多线程间的数据隔离问题。
java技术爱好者
2021/08/27
5300
重温并发知识,从ThreadLocal开始
相关推荐
ThreadLocal企业中真实应用
更多 >
加入讨论
的问答专区 >
1架构师擅长4个领域
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验