前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发——ThreadLocal(十二)

Java并发——ThreadLocal(十二)

原创
作者头像
翰墨飘香
发布2024-08-14 22:40:16
740
发布2024-08-14 22:40:16
举报
文章被收录于专栏:Java并发

一、ThreadLocal是什么

ThreadLocal 用于解决多线程环境下的线程安全问题。ThreadLocal为每个线程访问的变量提供了一个独立的副本,线程在访问这个变量时,访问的都是自己的副本数据,从而线程安全,即ThreadLocal为变量提供了线程隔离。

参考:https://juejin.cn/post/7336822616386846754

https://blog.csdn.net/qq_52173163/article/details/125529524

二、ThreadLocal使用

2.1 使用场景

1. 线程间数据隔离

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

2. 上下文传递

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

2.2 如何使用

ThreadLocal() 构造函数,创建ThreadLocal对象

get():T 获取当前线程绑定的局部变量

set(value:T) 设置当前线程绑定的局部变量

remove() 移除当前线程绑定的局部变量

2.3 使用demo

1. 线程数据隔离demo

下面代码使用了 ThreadLocal 帮每个线程去生成它自己的 simpleDateFormat 对象,对于每个线程而言,这个对象是独享的。但与此同时,这个对象就不会创造过多,一共只有 16 个,因为线程只有 16 个

代码语言:java
复制
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo1().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);

        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();

        return dateFormat.format(date);

    }

    static class ThreadSafeFormatter {
        static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("mm:ss");
            }
        };
    }

2. 上下文传递demo

每个线程内需要保存类似于全局变量的信息(例如订单信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的订单信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 order 对象)作为参数传递的麻烦。

代码语言:java
复制
public class CashierOrderContextHolder {
    public static ThreadLocal<Order> holder = new ThreadLocal<>();
}
代码语言:java
复制
@Data
public class Order {
    /**
     * 订单Id
     */
    private String orderId;

    /**
     * 用户id 
     */
    private String userId;
}
代码语言:java
复制
public class Service1 {
    public void method1() {
        Order order = new Order();
        order.setOrderId(orderId);
        order.setUserId(userId);
        CashierOrderContextHolder.holder.set(order);   
    }
}      
代码语言:java
复制
public class Service2 {
    public void method2() {
        Order order = CashierOrderContextHolder.holder.get();
        System.out.println("Service2拿到订单Id:" + order.getOrderId());
    }
}

三、ThreadLocal和Sychronized

原理

特点

ThreadLocal

ThreadLocal 采用以 空间换时间 的方式,为每个线程都提供一份变量副本,避免了资源的竞争

数据互相隔离,可避免传参,不适合数据共享场景

synchronized

同步机制采用以 时间换空间 的方式,只有一份变量,让不同的线程排队访问,主要用于临界资源的分配

多线程访问资源同步,效率低一些,花费内存少

四、Thread、ThreadLocal和ThreadLocalMap关系

用一张图展示ThreadLocal、ThreadLocalMap、Thread的关系

4.1 Thread类源码分析

Thread类里面有一个ThreadLocalMap类型的变量

代码语言:java
复制
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

4.2 ThreadLoca源码分析

ThreadLocalMap

ThreadLocal类,可以看到里面有一个ThreadLocalMap静态内部类,也就是上面Thread里面threadLocals变量将要指向的对象。

从 ThreadLocal 的常用函数可以发现,Thread 并没有在初始化的时候创建 ThreadLocalMap 对象,而是在 ThreadLocal 调用 set()/get() 时,通过 createMap(Thread, T) 创建;

为什么要这样设计呢?

ThreadLocal通过给ThreadLocalMap使用默认的权限修饰符,使得ThreadLocalMap无法被其他包的类引用,最终将ThreadLocalMap完美地隐藏起来,同时ThreadLocal提供了一系列操作容器ThreadLocalMap的方法(get、set等),供外界使用。

代码语言:java
复制
public class ThreadLocal<T> {
     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 Entry[] table;

       
        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();
        }
        }
    }
}

ThreadLocal.set方法

ThreadLocal set流程本质上就是设置当前线程对应的ThreadLocal的值:

1、 获取当前线程 Thread;

2、获取当前线程对应的 ThreadLocalMap;

3、赋值:

threadLocals 非空: 将value 存储到 ThreadLocalMap中(调用了ThreadLocalMap.set方法),key 为 ThreadLocal;

threadLocals 为空,先调用createMap创建 ThreadLocalMap 对象,并赋初始值 value;

代码语言:java
复制
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //这里调用的是ThreadLocalMap.set
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }    

ThreadLocal.get方法

先获取当前线程对象,然后拿到当前线程对象的ThreadLocalMap,在根据键(即当前对象ThreadLocal)找到值

如果我们在获取的时候当前线程对象的ThreadLocalMap为null时,则会执行setInitialValue()方法。

做了三件事:

1.创建map

2.给map设置一个键值对{threadLocal : initialValue}

3.返回initialValue,默认null

通过上面的源码我们可以知道,ThreadLocalMap以ThreadLocal作为键,值是我们调用ThreadLocal的set方法传进来的。

代码语言:java
复制
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
代码语言:java
复制
    private T setInitialValue() {
        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);
        }
        return value;
    }

4.3 总结

Thread类里面有一个ThreadLocalMap类型的变量,但是外界无法直接操作这个ThreadLocalMap,提供了一个工具箱ThreadLocal帮助我们操作ThreadLocal

1、 ThreadLocal的作用有两个

1)工具类,提供一系列方法操作ThreadLocalMap,比如get/set/remove

2)隔离Thread和ThreadLocalMap,防止程序员直接创建ThreadLocalMap。

自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续用,没有就为其绑定。

2、虽然ThreadLocalMap是ThreadLocal的静态内部类,但它们的实例对象并不存在继承或者包裹关系。完全可以当成两个独立的实例;

3、一个Thread只能有一个ThreadLocalMap;

4、ThreadLocalMap以ThreadLocal为键存储数据。

五、ThreadLocal内存泄漏问题

5.1 什么是内存泄漏

内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。

通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。

ThreadLocal为啥会内存泄漏

从引用关系图可以看到 ThreadLocalMap 作为 Thread 的属性,其生命周期是跟 Thread 一样长,假设 ThreadLocal 被回收,而线程还未结束,那么 ThreadLocalMap 中对应的 Entry.key 会被置为 null,此时这个 entry.value 在线程生命周期内不会再次被访问,如果线程是复用的,那么该 ThreadLocalMap 内部就会存在一个或多个 entry(null, value) 对象,从而导致内存泄漏;

怎么防范

调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏

代码语言:java
复制
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);

}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、ThreadLocal是什么
  • 二、ThreadLocal使用
    • 2.1 使用场景
      • 1. 线程间数据隔离
      • 2. 上下文传递
    • 2.2 如何使用
      • 2.3 使用demo
        • 1. 线程数据隔离demo
        • 2. 上下文传递demo
    • 三、ThreadLocal和Sychronized
    • 四、Thread、ThreadLocal和ThreadLocalMap关系
      • 4.1 Thread类源码分析
        • 4.2 ThreadLoca源码分析
          • ThreadLocalMap
          • ThreadLocal.set方法
          • ThreadLocal.get方法
        • 4.3 总结
        • 五、ThreadLocal内存泄漏问题
          • 5.1 什么是内存泄漏
            • ThreadLocal为啥会内存泄漏
              • 怎么防范
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档