并发场景下,多个线程同时读写共享变量就有可能产生并发安全问题。反过来也可以说,不存在共享变量,就不会出现线程安全问题。Java中有两种常用的避免共享变量的方法,使用局部变量,以及使用 ThreadLocal。

局部变量存在于每个线程内部的调用栈中,多个线程之间互相访问不到对方的局部变量,这就叫做线程封闭。如下图所示,局部变量存在于线程各自的调用栈中,线程之间互不打扰。

采用局部变量的方案,的确避免了变量被多个线程共享,同时它也禁止同一个线程中不同方法共享这个变量。然而,单线程中不同的方法之间共享变量是不会导致线程安全问题的。


如果想让同一个线程,不同的方法共享变量就可以使用 ThreadLocal,Java 提供的线程本地存储方案。ThreadLocal 可以保证同一个变量,该线程中的方法看到的值是一样,不同线程之间却是隔离。


ThreadLocal 的使用方法

常规使用 ThreadLocal 的方式很简单,创建一个 ThreadLocal 对象,然后调用它的 set(value)方法设置值,再调用 get() 方法获取这个 ThreadLocal 对象对应的value。

// 创建一个 ThreadLocal
ThreadLocal<String> tl = new ThreadLocal<>();
// set方法
tl.set("深页");
// get方法
tl.get();

ThreadLocal 类的注释中还带有为每个线程分配自增 id 的示例代码。withInitial()方法会调用initialValue()方法,为 ThreadLocal 设置 get() 的初始值。执行下面的代码,可以看到每个类都有自己的id,并且id的自增的。

public class ThreadId {// Integer类型的原子类,用来分配Id,保证其本身是线程安全的private static final AtomicInteger nextId = new AtomicInteger();// 创建一个 ThreadLocal 变零,并且为其赋值private static ThreadLocal <Integer> threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());// 获取id,即从ThreadLocal中获取对应的值public static int getId() {return threadId.get();}public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() ->System.out.println(Thread.currentThread().getName()+ ": " + ThreadId.getId())).start();}}
}

ThreadLocal还有一个经典的使用案例,就是将线程不安全的 SimpleDateFormat 类封装成线程安全的,原理其实和上面的例子是一样:

static class SafeDateFormat {static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));static DateFormat get() {return tl.get();}
}

ThreadLocal的底层原理

先来看 set() 方法:

  1. 首先获取当前线程,然后通过当前线程获取线程持有的局部变量 threadLocals
  2. 如果返回的 map 不是空的就设置值
  3. 如果返回的 map 是空的,就调用构造方法初始化 map 并为其设置值
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

再看 get() 方法:

public T get() {Thread t = Thread.currentThread();// getMap()返回当前线程的threadLocalsThreadLocalMap map = getMap(t);if (map != null) {// map存在返回当前ThreadLocal对应的value值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// map不存在就初始化return setInitialValue();
}

看过上面两个方法,可以看到它们除了涉及到 Thread 类,还涉及到了一个类 ThreadLocalMap。那么 Thread、ThreadLocal、ThreadLocalMap 之间是什么关系呢?

ThreadLocalMap 是 ThreadLocal 的静态内部类,ThreadLocalMap 的底层是一个 Entry[] table 数组,Entry 是 ThreadLocalMap 的静态内部类,以 ThreadLocal 作为 key,以设置的值作为 value,如下所示:

static class Entry extends WeakReference <ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

Thread 持有一个 ThreadLocalMap 的引用 threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

所以说,我们通过当前线程 Thread t 可以到 t 持有的 ThreadLocalMap,并且通过 ThreadLocal 对象返回其对应的 value。

ThreadLocal 内存泄露问题

使用 Thread.start() 方法是不会产生内存泄露的问题的,只有当我们在线程池中使用 ThreadLocal 才有可能产生内存泄露问题。

内存泄露的本质是长生命周期的对象,持有短生命周期对象。当短生命周期的对象使用结束之后,理应被垃圾回收器回收,但是它却被一个更长生命周期的对象引用。通过可达性分析算法,该短生命周期的对象被一个GC Root引用,理应被回收的它就无法被回收。

**那为什么在线程池中使用ThreadLocal就可能发生内存泄露的问题呢?**我们就从长生命周期的对象,持有短生命周期对象这个角度进行分析。

线程池作为一种池化资源技术,目的是避免线程的频繁创建和销毁。一般来说,线程池中的线程生命周期都很长,是和应用程序同生共死的。这就意味着,被 Thread 持有的 ThreadLocalMap 一直都不会被回收。

ThreadLocalMap 底层是一个 Entry 数组,Entry是<ThreadLocal,value>对结构。Entry 对 ThreadLocal 是弱引用(WeakReference),所以ThreadLocal 生命周期之后,是结束是可以被回收掉的。但是 Entry 对 value 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

InheritableThreadLocal 与继承性

使用 ThreadLocal 还有这样一种需求,ThreadLocal 创建了线程变量 V,然后希望该线程创建的子线程也能访问到父线程的线程变量 V。

为此 Java 提供了 InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 继承自 ThreadLocal,用法其实和 ThreadLocal 一样。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {


最后做一个小结,多线程同时读写共享变量就有可能产生并发问题。一种解决并发问题的思路就是避免变量被共享。与之对应的技术有线程隔离(局部变量),以及线程本地存储ThreadLocal

相比于使用局部变量,ThreadLocal 存储的变量可以供线程中的方法共享,单线程对共享变量的读写必定是线程安全的。

线程安全的map_ThreadLocal | 线程本地存储相关推荐

  1. Java中的线程本地存储

    开发人员中鲜为人知的功能之一是线程本地存储. 这个想法很简单,并且在需要数据的情况下很有用. 如果我们有两个线程,则它们引用相同的全局变量,但我们希望它们具有彼此独立初始化的单独值. 大多数主要的编程 ...

  2. java线程本地存储_[并发并行]_[C/C++]_[使用线程本地存储Thread Local Storage(TLS)-win32和pthread比较]...

    场景: 1.  需要统计某个线程的对象上创建的个数. 2. 当创建的堆空间需要根据线程需要创建和结束时销毁时. 3. 因为范围是线程只能看到自己的存储数据,所以不需要临界区或互斥量来维护自己的堆内存. ...

  3. linux 有线程本地存储 (tls)?,有没有办法确定Linux上的库使用的线程本地存储模型...

    我自己遇到了这个错误,在调查时,我来了一个 mailing list post with this info: If you link a shared object containing IE-mo ...

  4. c语言 多个线程对同一变量执行memcpy_你可曾听过网络编程中应用线程本地存储?...

    壹:你可曾听过线程本地存储? 1. 什么是线程本地存储? 线程本地存储:thread local storage(简称TLS).也叫线程特有存储:thread specific storage(简称T ...

  5. 聊聊Linux中的线程本地存储(1)——什么是TLS

    从本篇开始进入另一个话题:线程本地存储(Thread Local Storage),在介绍这个概念前先说说变量和多线程的相关知识. 多线程下的变量模型 在单线程模型下,变量定义有两个维度,那就是在何处 ...

  6. Disruptor本地线程队列_实现线程间通信---线程间通信工作笔记001

    Disruptor本地线程队列_实现线程间通信---线程间通信工作笔记001 看到同事用这个东西了,这个挺好用的说是,可以实现,本地线程间的通信,好像在c++和java中都可以用 现在没时间研究啊,暂 ...

  7. java线程 同步与异步 线程池

    1)多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线 程的处理的数据,而B线程又修改了A线程处理的数理.显然这是由于全局资源造成的,有时为了解 决此问题,优先考虑 ...

  8. 【Java 并发编程】线程简介 ( 并发类型 | 线程状态 | CPU 数据缓存 )

    文章目录 一.并发类型 二.线程状态 三.CPU 数据缓存 一.并发类型 并发类型 : Thread Runnable Future ThreadPool 其中 Runnable , ThreadPo ...

  9. 【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存

    示例代码下载 : http://download.csdn.net/detail/han1202012/8638801; 一. 崩溃日志本地存储 1. 保存原理解析 崩溃信息本地保存步骤 : -- 1 ...

最新文章

  1. ConcurrentHashMap之实现细节
  2. C# Thread开启线程几种方式
  3. 印尼发生洪灾和山体滑坡 致多人死亡数千人撤离
  4. 串口开发,数据类型转换——字符串转 byte[],byte[]转二进制,二进制转十进制转byte[],byte[]转十进制,byte[]拼接,校验
  5. java的代理Proxy.newProxyInstance
  6. Tcl Tutorial 笔记4 ·if
  7. Linux 与 Windows 计算文件夹大小
  8. C陷阱与缺陷(二)语义“陷阱”、连接
  9. java实现一码多扫支付_详解JAVA后端实现统一扫码支付:微信篇
  10. aspcms标签大全
  11. 中文 APB Artist Sessions Presents- SHAUN BARRETT
  12. Unity 3d 摄像头
  13. php 文字转unicode,php汉字如何转unicode
  14. JAVA渣渣感悟——三目运算符(三元运算符)的注意事项
  15. 社区发现算法原理与louvain源码解析
  16. 点云处理--点云平移和旋转
  17. public、private、protected的区别
  18. kafka安装(windows版)
  19. 信息奥赛一本通1208:2的幂次方表示
  20. vue-barcode生成条形码

热门文章

  1. 树莓派:django,uwsgi,nginx安装与设置
  2. orale的tnsping与TCP/IP的ping命令的比较
  3. Shell-06 函数
  4. log4j警告:WARN Please initialize the log4j system properly 的解决方法
  5. HashSet/HashMap 存取值的过程
  6. Eclipse版本列表
  7. Mybatis学习记录(二)----mybatis开发dao的方法
  8. FCKeditor所有版本任意文件上传缺陷
  9. Fedora配置网络DHCP
  10. 人的一生应当这样度过