并发编程七、ThreadLocal、CopyOnWrite、ForkJoin
前言:
- 文章内容:线程与进程、线程生命周期、线程中断、线程常见问题总结
- 本文章内容来源于笔者学习笔记,内容可能与相关书籍内容重合
- 偏向于知识核心总结,非零基础学习文章,可用于知识的体系建立,核心内容复习,如有帮助,十分荣幸
- 相关文献:并发编程实战、计算机原理
ThreadLocal解析
概念:
线程本地变量,为每一个线程维护一个独立的变量副本,将对象可见范围限制在同一个线程内。synchronized同步机制采用时间换空间,共享变量让不同的线程排队访问。而ThreadLocal是空间换时间,为每个线程都提供一个变量的副本,实现同时访问而不干扰
使用场景:
- 每个线程需要一个独享的对象,通常是工具类,典型需要使用的类有SimpleDateFormat和Random。
- 比如:使用SimpleDateFormat工具类,线程池执行任务要求打印每个线程执行的时间,但是多线程会出现时间错乱,使用ThreadLocal让SimpleDateFormat变为每个线程都有一份。
- 每个线程内需要保存全局变量,如拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦。
- 比如:请求会进入多个服务的多个方法,我们频繁需要获取用户信息来进行操作,层层传递这个用户信息会导致代码冗余且不易维护。用ThreadLocal保存一些业务内容,这些信息在同一个线程池内使用,在线程的生命周期里都通过这个静态ThreadLocal实例的get方法去取得set的对象,避免了这个对象作为参数传递的麻烦。
使用优势:
- 传递数据 :保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
- 线程隔离 :各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
结构:
每个Thread维护了一个ThreadLocalMap,这个Map的key是ThreadLoacl实例本身,value是要存储的值。
源码分析:
ThreadLocalMap:ThreadLocal的静态内部类,定义了一个Entry来保存数据,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) //如果map不为null,存储此实体entry map.set(this, value); else //如果map,代表此线程不存在ThreadLocalMap对象,那就初始化ThreadLocalMap,并将当前线程和value作为第一个entry存储至ThreadLocalMap createMap(t, value); } public T get() { Thread t = Thread.currentThread(); //获取当前线程对象的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) { //如果存在,则以当前的ThreadLoacl实例为key,获取entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value;//获取value return result; } } //初始化:map不存在,此Thread对象没有ThreadLocalMap对象,map存在,但是没有与当前ThreadLoacl关联的entry return setInitialValue(); }
普及一下引用的回收
- 强引用:GC永远不会回收被引用的对象,内存不足抛异常都不回收。
- 软引用:内存不足时,会回收关联的对象
- 弱引用:垃圾回收会回收关联的对象
- 虚引用:关联对象随时可能被回收
内存泄漏问题:
- key使用强引用:使用完ThreadLocal需要回收,但是ThreadLocalMap的Entry强引用了ThreadLocal,导致无法被回收。在没有删除这个entry及这个ThreadLocalMap仍在运行的情况下,Entry就不会被回收,导致Entry内存泄漏。
- key使用弱引用:使用完ThreadLocal需要回收,但是ThreadLocalMap持有ThreadLocal的弱引用,那么ThreadLocal可以被GC回收,此时entry中的key=null。但是在没有删除这个entry及这个ThreadLocalMap仍在运行的情况下,这个value不会被回收,而key回收了,value永远不会被访问,导致value内存泄漏。
- 内存泄漏的原因:与key是强弱引用没有关系,真正原因是entry没有删除,且ThreadLocalMap仍在运行。
- 解决:entry的删除采用remove方法就可以避免内存泄漏。由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal之后,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
- 为什么使用弱引用:在ThreadLocalMap中的set/getEntry方法中,会对key为null进行判断,如果为null的话,那么是会对value置为null的。使用弱引用多了一层保障,如果使用完ThreadLocal忘记了remove,那么对于的value在下一次调用set、get、remove任一方法时会被清除,避免内存泄漏。
CopyOnWriteArrayList(写时复制)
实现原理:
往容器添加元素时,不直接往容器添加,而是复制当前容器的拷贝,往新容器添加元素,最后将原容器的引用指向新容器。对list和set在并发下的操作,可以使用这种容器。
CopyOnWrite容器:
- 代替Vector和SynchronizedList,类似于ConcurrentHashMap代替SynchronizedMap一样。
- Vector和SynchronizedList的锁粒度太大,并发效率相对比较低,并且迭代时无法编辑
- CopyOnWrite容器还包括CopyOnWriteArraySet,用来代替同步Set
- 该容器是读写分离的,写操作执行过程中,读不会阻塞,但是读取的是老容器数据。
CopyOnWriteArrayList优缺点:
- 优点:用于读多写少并发场景,线程安全的list容器。迭代器遍历时进行修改,不会抛出异常。
- 缺点:每次执行写操作都会将原容器拷贝一份,数据量过大时内存有压力。只能保证数据的最终一致性,不能保证数据的实时一致性。如果希望写入的数据马上能读到,请不要使用该容器
CopyOnWriteArrayList适用场景:
- 读操作尽可能快,写慢一点没事:如黑名单、每日更新;监听器:迭代操作远多于修改操作
- 读写锁升级:读取不加锁,写入也不阻塞读取操作,只有写入和写入之间需要进行同步
ForkJoin
ForkJoin概念:
适合于CPU密集型任务处理,核心思想是分而治之。将一个大任务分为多个小任务去执行,最后进行汇总,提升计算效率。ForkJoin在执行任务时使用了工作窃取算法。
工作窃取算法:
- 多线程执行不同任务队列,某个线程执行完自己的队列后,从其他线程的队列中窃取任务来执行
- 窃取时,为了减少线程的竞争,采用双端队列存储任务,被窃取的线程从队头拿任务,窃取的线程从队尾拿任务
- 当一个线程窃取任务时没有其他可用任务了,会进入阻塞状态
原理:
实现ExecutorSerivce接口的多线程处理器,专为可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。与其他ExecutorSerivce实现相同的是,Fork/Join框架会将任务分配给线程池中的线程。而与之不同的是,Fork/Join框架在执行任务时使用了工作窃取算法。
原理图示:

本文来自博客园,作者:难得,转载请注明原文链接:https://www.cnblogs.com/zhangbLearn/p/16638756.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2018-08-30 Rabbitmq-topic演示