ThreadLocal使用场景与原理

╰半橙微兮° 2023-02-21 09:10 49阅读 0赞

目录

ThreadLocal的使用场景

ThreadLocal与synchronized的区别

Thread、ThreadLocal及ThreadLocalMap的关系

ThreadLocal为什么可能产生内存泄漏,如何避免?

子线程如何共享主线程的ThreadLocal变量?


ThreadLocal的使用场景

  • ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
  • ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

通过例子验证一下:

我们知道SimpleDateFormat在多线程并发访问下会出现线程安全问题。

  1. /**
  2. * 线程不安全demo
  3. *
  4. * @author hujy
  5. * @version 1.0
  6. * @date 2020-06-29 20:57
  7. */
  8. public class ThreadNotSafeDemo {
  9. private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
  10. static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
  11. public String date(int seconds) {
  12. // 创建不同的date
  13. Date date = new Date(1000 * seconds);
  14. return dateFormat.format(date);
  15. }
  16. public static void main(String[] args) {
  17. for (int i = 0; i < 1000; i++) {
  18. int finalI = i;
  19. threadPool.submit(() -> {
  20. String date = new ThreadNotSafeDemo().date(finalI);
  21. System.out.println(date);
  22. });
  23. }
  24. threadPool.shutdown();
  25. }
  26. }

打印运行结果:

20200629211646377.png

上面代码中每次循环都会创建不同的date对象,但是在多线程并发创建的场景下,打印的结果中出现了大量重复值,说明产生了线程安全问题。

通过ThreadLocal保证线程安全:

  1. /**
  2. * ThreadLocal线程安全demo
  3. *
  4. * @author hujy
  5. * @version 1.0
  6. * @date 2020-06-29 21:19
  7. */
  8. public class ThreadSafeDemo {
  9. private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
  10. public String date(int seconds) {
  11. Date date = new Date(1000 * seconds);
  12. SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
  13. return dateFormat.format(date);
  14. }
  15. public static void main(String[] args) {
  16. for (int i = 0; i < 1000; i++) {
  17. int finalI = i;
  18. threadPool.submit(() -> {
  19. try {
  20. String date = new ThreadSafeDemo().date(finalI);
  21. System.out.println(date);
  22. } finally {
  23. ThreadSafeFormatter.dateFormatThreadLocal.remove();
  24. }
  25. });
  26. }
  27. threadPool.shutdown();
  28. }
  29. }
  30. class ThreadSafeFormatter {
  31. public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
  32. ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
  33. }

打印运行结果:

20200629212517907.png

运行结果都是唯一的值,说明通过ThreadLocal可以实现共享变量的线程安全。


ThreadLocal与synchronized的区别

  • ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
  • synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。
  • ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。

Thread、ThreadLocal及ThreadLocalMap的关系

想要了解Threadlocal的工作原理,就必须了解Thread、ThreadLocal以及ThreadLocalMap这三个类之间的关系。

ThreadLocalMap是ThreadLocal类的静态内部类,本质是一个Map,key的类型就是我们定义的ThreadLocal对象,value则是我们具体要保存的变量参数。

  1. public class ThreadLocal<T> {
  2. ...
  3. static class ThreadLocalMap {
  4. /**
  5. * The entries in this hash map extend WeakReference, using
  6. * its main ref field as the key (which is always a
  7. * ThreadLocal object). Note that null keys (i.e. entry.get()
  8. * == null) mean that the key is no longer referenced, so the
  9. * entry can be expunged from table. Such entries are referred to
  10. * as "stale entries" in the code that follows.
  11. */
  12. static class Entry extends WeakReference<ThreadLocal<?>> {
  13. /** The value associated with this ThreadLocal. */
  14. Object value;
  15. Entry(ThreadLocal<?> k, Object v) {
  16. super(k);
  17. value = v;
  18. }
  19. ...
  20. private Entry[] table;
  21. }
  22. }

而Thread中含有ThreadLocal.ThreadLocalMap类型的成员变量threadLocals。

  1. public class Thread implements Runnable {
  2. ...
  3. ThreadLocal.ThreadLocalMap threadLocals = null;
  4. ...
  5. }

因此这三个类的关系可以总结为:

一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

另外ThreadLocalMap在解决hash冲突的方式与HashMap不同,HashMap采用的是拉链发,而ThreadLocalMap采用的是线性探索法,即发生冲突时,向下继续寻找空的位置。


ThreadLocal为什么可能产生内存泄漏,如何避免?

#

通过ThreadLocalMap的源码可以看到,Entry中的key被定义为弱引用类型,当发生GC时,key会被直接回收,无需手动清理。

而value属于强引用类型,被当前的Thread对象关联,所以说value的回收取决于Thread对象的生命周期。如果说一个线程执行完毕,线程Thread随之被释放,那么value便不存在内存泄漏的问题。然而,我们一般会通过线程池的方式来复用Thread对象来节省资源,这就会导致一个Thread对象的生命周期会非常长,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

因此,我们在使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap(一般在finally代码块中)。

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }

子线程如何共享主线程的ThreadLocal变量?

因为ThreadLocal变量保存在当前线程的成员变量ThreadLocalMap中,新创建子线程后无法再次使用父线程的ThreadLocal变量;

为了解决子线程复用主线程ThreadLocal的问题,Thread类中还有另一个ThreadLocalMap:inheritableThreadLocals,里面保存的是InheritableThreadLocal,它是ThreadLocal的子类,Thread类初始化时会默认从父线程继承inheritableThreadLocals;

因此我们可以用InheritableThreadLocal代替ThreadLocal实现父子线程共享线程变量的问题。

发表评论

表情:
评论列表 (有 0 条评论,49人围观)

还没有评论,来说两句吧...

相关阅读