并发编程七、ThreadLocal、CopyOnWrite、ForkJoin

前言:

  1. 文章内容:线程与进程、线程生命周期、线程中断、线程常见问题总结
  2. 本文章内容来源于笔者学习笔记,内容可能与相关书籍内容重合
  3. 偏向于知识核心总结,非零基础学习文章,可用于知识的体系建立,核心内容复习,如有帮助,十分荣幸
  4. 相关文献:并发编程实战、计算机原理

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适用场景:

  1. 读操作尽可能快,写慢一点没事:如黑名单、每日更新;监听器:迭代操作远多于修改操作
  2. 读写锁升级:读取不加锁,写入也不阻塞读取操作,只有写入和写入之间需要进行同步

ForkJoin


ForkJoin概念:

  适合于CPU密集型任务处理,核心思想是分而治之。将一个大任务分为多个小任务去执行,最后进行汇总,提升计算效率。ForkJoin在执行任务时使用了工作窃取算法。

工作窃取算法:

  • 多线程执行不同任务队列,某个线程执行完自己的队列后,从其他线程的队列中窃取任务来执行
  • 窃取时,为了减少线程的竞争,采用双端队列存储任务,被窃取的线程从队头拿任务,窃取的线程从队尾拿任务
  • 当一个线程窃取任务时没有其他可用任务了,会进入阻塞状态

原理:

  实现ExecutorSerivce接口的多线程处理器,专为可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。与其他ExecutorSerivce实现相同的是,Fork/Join框架会将任务分配给线程池中的线程。而与之不同的是,Fork/Join框架在执行任务时使用了工作窃取算法。

原理图示:

 

 

 

posted @ 2022-08-30 11:35  难得  阅读(66)  评论(0编辑  收藏  举报