Java并发——ThreadLocal(十二)

2024-08-14 22:40:16 浏览数 (3)

一、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);

}

0 人点赞