Java高并发编程详解系列-线程上下文设计模式及ThreadLocal详解

 2023-09-15 阅读 24 评论 0

摘要:导语   在之前的分享中提到过一个概念就是线程之间的通信,都知道在线程之间的通信是一件很消耗资源的事情。但是又不得不去做的一件事情。为了保证多线程线程安全就必须进行线程之间的通信,保证每个线程获取到的数据都是一样的。那么就需要知道线程上下文&#x

导语
  在之前的分享中提到过一个概念就是线程之间的通信,都知道在线程之间的通信是一件很消耗资源的事情。但是又不得不去做的一件事情。为了保证多线程线程安全就必须进行线程之间的通信,保证每个线程获取到的数据都是一样的。那么就需要知道线程上下文,对于线程上下文来讲就是线程的依托。

文章目录

    • 什么是上下文
    • 如何设计线程上下文
    • ThreadLocal 详解
      • ThreadLocal 的使用场景以及注意事项
      • ThreadLocal 的方法详解以及源码分析
      • 关于ThreadLocal内存泄露问题分析
    • 总结

什么是上下文

   关于上下文(context),在开发中经常会遇到,例如Spring中的上下文ApplicationContext,Struts2的ActionContext,对于上下文来说就是系统整个生命周期的依托,提供了一些全局信息,例如Spring 容器信息、请求的Request信息、以及在运行的某个阶段需要的运行时数据等。它贯穿了整个的程序运行的生命周期。

public class ApplicationContext {private ApplicationConfiguration configuration;private RuntimeInfo runtimeInfo;private static class Holder{private static ApplicationContext instance = new ApplicationContext();}public static  ApplicationContext getContext(){return Holder.instance;}public ApplicationConfiguration getConfiguration() {return configuration;}public void setConfiguration(ApplicationConfiguration configuration) {this.configuration = configuration;}public RuntimeInfo getRuntimeInfo() {return runtimeInfo;}public void setRuntimeInfo(RuntimeInfo runtimeInfo) {this.runtimeInfo = runtimeInfo;}
}

  在上面代码中会看到configuration和runtimeinfo的整个生命周期会随着被创建一直到系统运行结束,这里就可以将ApplicationContext称为是系统上下文,例如configuration和runtimeinfo的就被称为是上下文中的成员。
  在设计系统上下问的时候,除了要考虑全局唯一性(单例设计模式可以保证)之外,还要考虑的就是其成员也是只能被初始化一次的,例如有些情况下整个系统的配置信息是不能被改变的,在多线程场景下,上下文成员线程之间的安全性一定要得到保证。

如何设计线程上下文

并发线程数多少合适。  有些情况下,在单个线程执行的任务后续的步骤有很多,而且后一个步骤的输入有可能是前一个步骤的输出,比例在单个线程执行多个步骤的时候为了使得这个线程功能单一会采用一种设计模式叫做责任链设计模式,

next
next
next
步骤1
步骤2
步骤3
步骤4

  虽然在有些时候后一个步骤未必会有需要前一个步骤的输出结果,但是都需要将context上下文从头到尾的进行传递,假如方法需要传入的参数较少的情况下这种参数传递是是可以做到的,但是如果在有些参数较多的场景下,如果进行参数传递这种操作的话就会出现代码冗余度过高的问题。这个时候就可以用到线程上下文这种方式了。如下

public class Context {private ConcurrentHashMap<Thread,ApplicationContext> contexts = new ConcurrentHashMap<>();public ApplicationContext getContext(){ApplicationContext context = contexts.get(Thread.currentThread());if (context==null){context = new ApplicationContext();contexts.put(Thread.currentThread(),context);}return context;}
}

  不同的线程访问到getContext()方法的时候,每个线程都会获取到不一样的Context,原因是采用了Thread.currentThread作为contexts的key值,这样可以保证线程上下文之间是独立的,同时也不需要考虑线程上下文之间的安全性,所以线程上下文其实被称为是线程级别的单例。从上面代码中也可以感觉到。它采用了单例模式来进行实现。

注意
  通过这种方式定义线程上下问题很有可能会导致内存泄露,contexts是一个Map的数据结构,用当前线程作为key,当线程生命周期结束后,contexts中的Thread实例不会被释放,与之对应的Value也就不会被释放,时间太长的话就会导致内存泄露(Memory Leak),当然在另一个方面可以使用soft reference 或者是 weak reference 等引用类型,JVM会尝试主动进行回收。对于引用类型可以参考博主的关于JVM相关的文章来进行学习。

ThreadLocal 详解

  在面试的过程中经常会被问到多线程相关的知识,这个时候就不得不提到一个ThreadLocal的东西。自从JDK1.2开始Java就提供了一个java.lang.ThreadLocal的类,ThreadLocal为每个使用该变量的线程都提供了一个独立的副本,可以做到线程之间的数据隔离,每个线程都可以访问各个线程内部的副本变量。这种操作类似于操作线程私有的内存一样。

ThreadLocal 的使用场景以及注意事项

  ThreadLocal在Java开发中非常常见,一般在以下的情况下会使用到

  • 在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次之间的约束
  • 线程间数据的隔离,例如在上面提到的线程上下文
  • 进行事务操作,用户存储线程事务信息

检测线程上下文注入,  ThreadLoca 并不是解决多线程下共享资源的技术,一般情况下,每个线程的ThreadLocal存储的都是一个全新的对象(通过New关键字创建的),如果多线程的ThreadLocal存储了一个对象的引用,那么它还是会面临资源竞争的问题,还是会导致数据不一致等并发问题。

ThreadLocal 的方法详解以及源码分析

  在分析源码之前首先来看一个小例子。

public class ThreadLocalExample {public static void main(String[] args) {ThreadLocal<Integer> threadLocal = new ThreadLocal<>();IntStream.range(0,10).forEach(i->new Thread(()->{try{threadLocal.set(i);System.out.println(currentThread()+"set i "+threadLocal.get());TimeUnit.SECONDS.sleep(1);System.out.println(currentThread()+"get i "+threadLocal.get());} catch (InterruptedException e) {e.printStackTrace();}}).start());}
}

  从上面的代码定义中可以看到只有一个全局唯一的ThreadLocal 的变量,然后后续启动了10个线程通过set和get方法进行了操作,通过运行程序得到下面这个结果,会发现这些结果之间并不会相互之间有什么影响。每个线程输入的值之间完全不同并且彼此独立。
在这里插入图片描述

  通过上面的例子对于ThreadLocal有了一些了解,在使用ThreadLocal的时候最常用的就是以下的一些方法

  • 1、initialVaue()方法
protected T initialValue() {return null;
}

  方法源码很简单,initialValue()方法为ThreadLocal要保存的数据类型指定了一个初始化的值,在ThreadLocal中默认返回值是null。
  但是我们可以通过重写initialValue()方法进行数据的初始化操作,例如下面代码所示,线程并没有对threadlocal进行set操作,但是还是可以通过get方法获取到对应的值。通过输出信息不难发现。

public class ThreadLocalTest {public static void main(String[] args) {ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){@Overrideprotected Object initialValue() {return new Object();}};new Thread(()-> System.out.println(threadLocal.get())).start();System.out.println(threadLocal.get());}
}
  • set()方法

  在之前的代码中也看到了关于set()方法的使用。set()方法主要是为了ThreadLocal制定将要被存储的数据,如果重写的initialValue()方法在不调用set()方法的时候回使用initialValue的值。上面已经看到,源码如下

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}

一个进程可以有多个线程,在这里插入图片描述
  根据上述代码可以看到,它是做了如下的一些操作

  • 1、获取当前线程的Thread.currentThread()

  • 2、根据当前线程获取与之相关的ThreadLocalMap数据结构

  • 3、如果map为null则进入第四步,否则就进入第五步

  • 4、当map为空的时候,创建一个ThreadLoaclMap ,用当前的ThreadLocal实例作为key,将所要存放的数据作为Value,对应到ThreadLocalMap中创建一个Entry。

  • 5、在map的set方法中遍历整个map的Entry,如果发现ThreadLocal相同则使用新数据替换,set过程结束

  • tomcat线程模型,6、在遍历map的entry过程中,发现Entry的key为null,则直接将其推出,并使用新数据占用被推出的数据的位置,整个过程主要是为了防止内存泄露在上面提到过

  • 7、创建新的entry,使用ThreadLocal作为key,将要存放的数据作为value。

  • 8、最后根据ThreadLocalMap当前元素大小和阈值做比较再次进行key为null的清理工作。

  • get()方法

  get方法用于返回当前线程在ThreadLocal中的数据备份,当前线程的数据都被存放到了一个ThreadLocalMap的数据结构中,后面会介绍这个数据结构,get源码如下
在这里插入图片描述
在这里插入图片描述
  通过上面的代码来简单的分析一下数据拷贝的过程

  • 1、首先获取当前线程Thread.currentThread()方法;
  • 2、根据Thread获取ThreadLocalMap,其中ThreadLocalMap与Thread是关联的,而且上面以及提到了我们存储的数据其实是在ThreadLocalMap的Entry中。
  • 3、如果map已经被创建过了,则当前的ThreadLocal作为key获取Value
  • 4、如果Entry不为null,则直接返回value,否则进入第五步
  • 5、如果第二步获取不到对应的ThreadLocalMap,则执行setInitialValue()方法
  • 6、在setInitialValue() 方法中首先通过initialValue()方法来获取初始值
  • 7、根据当前线程Thread获取对应的ThreadLocalMap
  • 8、如果ThreadLocalMap不为null。则为map指定的initialValue()获取的初始值,实际上在mapset方法中new了一个Entry对象。
  • 9、如果ThreadLocalMap为null也就是首次使用,则需要先进行创建,并且设置对应的属性,其实这里使用了一个懒加载的机制。
  • 10、返回initialValue()方法的结果,当然这个结果在没有被重写的情况下为null。

ThreadLocaMap
  无论是上面哪一种操作,都离不开一个对象ThreadLocalMap和其中的Entry。ThreadLocalMap是一个完全类似于HashMap的数据结构,仅仅用于存放线程存放在ThreadLocal中的数据备份,ThreadLocalMap的所有方法都是私有的。

线程与进程,  在ThreadLocalMap中用于实际存储数据的Entry,它其实是一个WeakReference类型的子类型,之所以被设置为WeakReference类型是为了在JVM垃圾回收发生时,可以自动防止垃圾回收内存溢出情况,关于WeakReference的相关内容可以参考博主的有关JVM系列的分享。通过Entry源码分析不难发现其实在Entry中是有ThreadLocal以及所需要的备份数据
在这里插入图片描述

关于ThreadLocal内存泄露问题分析

  前面一直提到过,在使用线程上下文这种方式的时候会有内存泄露的问题。其实ThreadLocal也是类似的,都使用了当前线程作为一个KV。但是在上面描述有关内存泄露问题的时候,例如某个线程的结束了生命周期但是实际上它的上下文信息还是存在于KV中,随着运行时间的增加,在上下文KV中会保留很多的上下文信息。

  这个问题从分析源码可以看到在ThreadLocal中也是存在的所以在存储的时候使用了WeakReference 由于WeakReference的特性在任何的GC操作中都会导致其回收,这个可以参考博主JVM相关的文章,这里有个问题WeakReference的特性在任何的GC操作中都会导致回收,回收之后执行get()操作又会怎么样呢?这个其实不用担心请看如下代码。通过对下面几段代码的分析,其实会发现ThreadLocalMap在一定程度上是保证不会发生内存泄露的
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
下面就来看看如下的一段测试

public class ThreadLocalOOM {public static void main(String[] args) throws InterruptedException {ThreadLocal<byte[]> threadLocal  = new ThreadLocal<>();TimeUnit.SECONDS.sleep(30);threadLocal.set(new byte[1024*1024*100]);threadLocal.set(new byte[1024*1024*100]);threadLocal.set(new byte[1024*1024*100]);threadLocal = null;currentThread().join();}
}

  首先代码定义了ThreadLocal<byte[]>的数据分别设置了100MB的数据,会发现如下的效果
在这里插入图片描述
  当Thread和ThreadLocal对象发生绑定关系之后,对象引用链如下图所示
在这里插入图片描述
  但是在代码中强制的将引用显示的设置为null;就会出现如下的效果。
在这里插入图片描述
  当ThreadLocal被显式的指定为空,执行GC操作,此时的对内存中的ThreadLocal被回收,同时在ThreadLocalMap中的Entry为null,但是Value不会被释放,除非当前线程结束生命周期再回被垃圾回收器回收。

多线程上下文切换,  内存泄露和内存溢出是两个不同的概念,内存泄露只是内存溢出的一个原因,但是两者并不是等价的,内存泄露更多的是因为程序中不再持有对于某个对象的引用。因为没有引用所以就导致该对象无法被垃圾收集器所回收,也就是说是因为Root引用链路是可达的,就如同上图中一样,ThreadLocal到Entry.value 的引用链路一样。

总结

  在上面的介绍中详细介绍了线程上下文ThreadLocal。很多的开源的框架中都可以看到ThreadLocal的影子,这里可能唯一欠缺的一段代码就是使用ThreadLocal来保存线程上下文的代码,其实不难发现如果将上面关于实现上下文操作的private ConcurrentHashMap<Thread,ApplicationContext> contexts = new ConcurrentHashMap<>();代码换成ThreadLocal的话就会是个很好的实现。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/1/60402.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息