关于java:精通高并发与多线程却不会用ThreadLocal

32次阅读

共计 7059 个字符,预计需要花费 18 分钟才能阅读完成。

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~ 看完记得给我来个三连哦!

本文次要介绍 ThreadLocal 的应用

如有须要,能够参考

如有帮忙,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

之前咱们有在并发系列中提到 ThreadLocal 类和根本应用办法,那咱们就来看下 ThreadLocal 到底是如何应用的!

ThreadLocal 简介

概念

ThreadLocal 类是用来提供线程外部的局部变量。这种变量在多线程环境下拜访(getset 办法拜访)时能保障各个线程的变量绝对独立于其余线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和上下文。

作用

  • 传递数据

提供线程外部的局部变量。能够通过 ThreadLocal 在同一线程,不同组件中传递公共变量。

  • 线程并发

实用于多线程并发状况下。

  • 线程隔离

每个线程的变量都是独立的,不会相互影响。

ThreadLocal 实战

1. 常见办法

  • ThreadLocal ()

构造方法,创立一个 ThreadLocal 对象

  • void set (T value)

设置以后线程绑定的局部变量

  • T get ()

获取以后线程绑定的局部变量

  • void remove ()

移除以后线程绑定的局部变量

2. 为什么要应用 ThreadLocal

首先咱们先看一组并发条件下的代码场景:

 @Data
 public class ThreadLocalTest {
  private String name;
 ​
  public static void main(String[] args) {ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() +
  "t 拿到数据:" + tmp.getName());
  });
  thread.setName("Thread-" + i);
  thread.start();}
  }
 }

咱们现实中的代码输入后果应该是这样的:

 /** OUTPUT **/
 Thread-0  拿到数据:Thread-0
 Thread-1  拿到数据:Thread-1
 Thread-2  拿到数据:Thread-2
 Thread-3  拿到数据:Thread-3

然而实际上输入的后果却是这样的:

 /** OUTPUT **/
 Thread-0  拿到数据:Thread-1
 Thread-3  拿到数据:Thread-3
 Thread-1  拿到数据:Thread-1
 Thread-2  拿到数据:Thread-2

程序乱了没有关系,然而咱们能够看到 Thread-0 这个线程拿到的值却是 Thread-1

从后果中咱们能够看出多个线程在拜访同一个变量的时候会出现异常,这是因为线程间的数据没有隔离!

并发线程呈现的问题?那加锁不就完事了!这个时候你三下五除二的写下了以下代码:

 @Data
 public class ThreadLocalTest {
 ​
  private String name;
 ​
  public static void main(String[] args) {ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {synchronized (tmp) {tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() 
  + "t" + tmp.getName());
  }
  });
  thread.setName("Thread-" + i);
  thread.start();}
  }
 }
 /** OUTPUT **/
 Thread-2 Thread-2
 Thread-3 Thread-3
 Thread-1 Thread-1
 Thread-0 Thread-0

从后果上看,加锁如同是解决了上述问题,然而 synchronized 罕用于多线程数据共享的问题,而非多线程数据隔离的问题。这里应用 synchronized 尽管解决了问题,然而多少有些不适合,并且 synchronized 属于重量级锁,为了实现多线程数据隔离贸然的加上 synchronized,也会影响到性能。

加锁的办法也被否定了,那么该如何解决?不如用 ThreadLocal 牛刀小试一番:

 public class ThreadLocalTest {
 ​
  private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 ​
  public String getName() {return threadLocal.get();
  }
 ​
  public void setName(String name) {threadLocal.set(name);
  }
 ​
  public static void main(String[] args) {ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() + 
  "t 拿到数据:" + tmp.getName());
  });
  thread.setName("Thread-" + i);
  thread.start();}
  }
 }

在查看输入后果之前,咱们先来看看代码产生了那些变动

首先多了一个 private static 润饰的 ThreadLocal,而后在 setName 的时候,咱们实际上是往 ThreadLocal 外面存数据,在 getName 的时候,咱们是在 ThreadLocal 外面取数据。感觉操作上也是挺简略的,然而这样真的能做到线程间的数据隔离吗,咱们再来看一看后果:

 /** OUTPUT **/
 Thread-1  拿到数据:Thread-1
 Thread-2  拿到数据:Thread-2
 Thread-0  拿到数据:Thread-0
 Thread-3  拿到数据:Thread-3

从后果上能够看到每个线程都能取到对应的数据。ThreadLocal 也曾经解决了多线程之间数据隔离的问题。

那么咱们来小结一下,为什么须要应用 ThreadLocal,与 synchronized 的区别是什么

  • synchronized

原理: 同步机制采纳 “ 以工夫换空间 ” 的形式,只提供了一份变量,让不同线程排队拜访

侧重点: 多个线程之间同步拜访资源

  • ThreadLocal

原理: ThreadLocal 采纳 “ 以空间换工夫 ” 的形式,为每个线程都提供了一份变量的正本,从而实现同时拜访而互不烦扰

侧重点: 多线程中让每个线程之间的数据互相隔离

3. 内部结构

从下面的案例中咱们能够看到 ThreadLocal 的两个次要办法别离是 set()get()

那咱们无妨猜测一下,如果让咱们来设计 ThreadLocal,咱们该如何设计,是否会有这样的想法:每个 ThreadLocal 都创立一个 Map,而后用线程作为 Mapkey,要存储的局部变量作为 Mapvalue,这样就能达到各个线程的局部变量隔离的成果。

这个想法也是没错的,晚期的 ThreadLocal 便是这样设计的,然而在 JDK 8 之后便更改了设计,如下:

设计过程:

  1. 每个 Thread 线程外部都有一个 ThreadLocalMap
  2. ThreadLocalMap 中存储着以 ThreadLocal 对象为 key,线程变量为 value
  3. Thread 外部的 Map 是由 ThreadLocal 保护的,由 ThreadLocal 负责向 Map 设置和获取线程的变量值
  4. 对于不同的线程,每次获取正本值时,别的线程并不能获取到线程的正本值,这样就会造成正本的隔离,互不烦扰

注: 每个线程都要有本人的一个 map,然而这个类就是一个一般的 Java 类,并没有实现 Map 接口,然而具备相似 Map 相似的性能。

通过这样实现看起来貌似会比之前咱们猜测的更加简单,这样做的益处是什么呢?

  • 每个 Map 存储的 Entry 数量就会变少,因为之前的存储数量由 Thread 的数量决定,当初是由 ThreadMap 的数量决定,在理论开发中,ThreadLocal 的数量要更少于 Thread 的数量。
  • Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能缩小内存的应用

4. 源码剖析

首先咱们先看 ThreadLocalMap 中有哪些成员:

如果你看过 HashMap 的源码,必定会感觉这几个特地相熟,其中:

  • INITIAL_CAPACITY:初始容量,必须是 2 的整次幂
  • table:存放数据的table
  • size:数组中 entries 的个数,用于判断 table 以后使用量是否超过阈值
  • threshold:进行扩容的阈值,表使用量大于它的时候会进行扩容

ThreadLocals

Thread 类中有个类型为 ThreadLocal.ThreadLocalMap 类型的变量 ThreadLocals,这个就是用来保留每个线程的公有数据。

ThreadLocalMap

ThreadLocalMapThreadLocal 的外部类,每个数据用 Entry 保留,其中的 Entry 用一个键值对存储,键为 ThreadLocal 的援用。

咱们能够看到 Entry 继承于WeakReference,这是因为如果是强援用,即便把 ThreadLocal 设置为 nullGC 也不会回收,因为 ThreadLocalMap 对它有强援用。

在没有手动删除这个 Entry 以及 CurrentThread 仍然运行的前提下,始终有强援用链 threadRef -> currentThread -> threadLocalMap -> entryEntry就不会被回收(Entry中包含了 ThreadLocal 实例和 value),导致Entry 内存透露。

那是不是就是说如果应用了 弱援用 ,就不会造成 内存泄露 呢,这也是不正确的。

因为如果咱们没有手动删除 Entry 的状况下,此时 Entry 中的 key == null,这个时候没有任何强援用指向 threaLocal 实例,所以 threadLocal 就能够顺利被 gc 回收,然而 value 不会被回收,而这块的 value 永远不会被拜访到,因而会导致 内存泄露

接下来咱们看下 ThreadLocalMap 的几个外围办法:

set 办法

首先咱们先看下源码:

 public void set(T value) {
  // 获取以后线程对象
  Thread t = Thread.currentThread();
  // 获取此线程对象中保护的 ThreadLocalMap 对象
  ThreadLocalMap map = getMap(t);
  // 判断 map 是否存在
  if (map != null)
  // 存在则调用 map.set 设置此实体 entry
  map.set(this, value);
  else
  // 如果以后线程不存在 ThreadLocalMap 对象则调用 createMap 进行 ThreadLocalMap 对象的初始化
  // 并将 t(以后线程)和 value(t 对应的值)作为第一个 entry 寄存至 ThreadLocalMap 中
  createMap(t, value);
 }
 ​
 ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
 ​
 void createMap(Thread t, T firstValue) {
  // 这里的 this 是调用此办法的 threadLocal
  t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

执行流程:

  • 首先获取以后线程,并依据以后线程获取一个 map
  • 如果获取的 map 不为空,则将参数设置到 map 中(以后 ThreadLocal 的援用作为 key
  • 如果 Map 为空,则给该线程创立 map,并设置初始值

get 办法

源码如下:

 public T get() {
  // 获取以后线程对象
  Thread t = Thread.currentThread();
  // 获取此线程对象中保护的 ThreadLocalMap 对象
  ThreadLocalMap map = getMap(t);
  // 如果此 map 存在
  if (map != null) {
  // 以以后的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
  ThreadLocalMap.Entry e = map.getEntry(this);
  // 对 e 进行判空 
  if (e != null) {@SuppressWarnings("unchecked")
  // 获取存储实体 e 对应的 value 值
  // 即为咱们想要的以后线程对应此 ThreadLocal 的值
  T result = (T)e.value;
  return result;
  }
  }
  return setInitialValue();}
 ​
 private T setInitialValue() {
  // 调用 initialValue 获取初始化的值
  // 此办法能够被子类重写, 如果不重写默认返回 null
  T value = initialValue();
  // 获取以后线程对象
  Thread t = Thread.currentThread();
  // 获取此线程对象中保护的 ThreadLocalMap 对象
  ThreadLocalMap map = getMap(t);
  // 判断 map 是否存在
  if (map != null)
  // 存在则调用 map.set 设置此实体 entry
  map.set(this, value);
  else
  // 如果以后线程不存在 ThreadLocalMap 对象则调用 createMap 进行 ThreadLocalMap 对象的初始化
  // 并将 t(以后线程)和 value(t 对应的值)作为第一个 entry 寄存至 ThreadLocalMap 中
  createMap(t, value);
  // 返回设置的值 value
  return value;
 }

执行流程:

  • 首先获取以后线程,依据以后线程获取一个 map
  • 如果获取的 map 不为空,则在 map 中以 ThreadLocal 的援用作为 key 来在 map 中获取对应的 Entry entry,否则跳转到 第四步
  • 如果 Entry entry 不为空,则返回 entry.value,否则跳转到 第四步
  • map 为空或者 entry 为空,则通过 initialValue 函数获取初始值 value,而后用 ThreadLocal 的援用和 value 作为 firstKeyfirstValue 创立一个新的 map

remove 办法

源码如下:

 ​
 public void remove() {
  // 获取以后线程对象中保护的 ThreadLocalMap 对象
  ThreadLocalMap m = getMap(Thread.currentThread());
  // 如果此 map 存在
  if (m != null)
  // 存在则调用 map.remove
  m.remove(this);
 }
 // 以以后 ThreadLocal 为 key 删除对应的实体 entry
 private void remove(ThreadLocal<?> key) {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)]) {if (e.get() == key) {e.clear();
  expungeStaleEntry(i);
  return;
  }
  }
 }

执行流程:

  • 首先获取以后线程,并依据以后线程获取一个 map
  • 如果取得的map 不为空,则移除以后 ThreadLocal 对象对应的 entry

initialValue 办法

源码如下:

 protected T initialValue() {return null;}

在源码中咱们能够看到这个办法仅仅简略的返回了 null,这个办法是在线程第一次通过 get () 办法拜访该线程的 ThreadLocal 时调用的,只有在线程先调用了 set () 办法才不会调用 initialValue () 办法,通常状况下,这个办法最多被调用一次。

如果们想要 ThreadLocal 线程局部变量有一个除 null 以外的初始值,那么就必须通过子类 继承 ThreadLocal 来重写此办法,能够通过匿名外部类实现。

【END】

这篇 ThreadLocal 就介绍到这里啦,心愿读到这里的小伙伴可能有所播种。

路漫漫,小菜与你一起求索!

明天的你多致力一点,今天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 ????

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

正文完
 0