线程池与ThreadLocal

·  阅读 283

一、线程池

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时,JVM会调用该类的 run方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。这就是线程池的实现原理。创建线程池通过ThreadPoolExecutor类实现。

使⽤线程池的好处

  • 降低资源消耗:通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
  • 提⾼响应速度:当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
  • 提⾼线程的可管理性:线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

核心参数

  • corePoolSize:指定了线程池中的线程数量。
  • maximumPoolSize:指定了线程池中的最大线程数量。
  • keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
  • unit:keepAliveTime 的单位。
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  • handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

线程池工作过程

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

  • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会根据拒绝策略决定行为。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  • 当一个线程无事可做,超过一定的时间keepAliveTime时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool: 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
  • CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。

JDK 内置的拒绝策略

  • AbortPolicy :默认,直接抛出异常,阻止系统正常运行。

  • CallerRunsPolicy :被拒绝的任务在主线程中运行,所以主线程就被阻塞了,别的任务只能在被拒绝的任务执行完之后才会继续被提交到线程池执行。

  • DiscardOldestPolicy :丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

  • DiscardPolicy :不处理新任务,直接丢弃掉。

以上内置拒绝策略均实现了RejectedExecutionHandler接口,若以上策略仍无法满足实际需要,可以自己扩展该接口。

二、ThreadLocal

ThreadLocal 的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。可以使⽤ get() 和 set() ⽅法来获取默认值或将其值更改为当前线程所存的副本的值。

public class Thread implements Runnable {
    // 与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
	......
}
复制代码

ThreadLocalMap 是当前线程 Thread 一个叫threadLocals的变量中获取的。ThreadLocalMap 理解为 ThreadLocal 类实现的定制化的 HashMap 。默认情况下这个变量是 null,只有当前线程调⽤ ThreadLocal 类的 set 或 get ⽅法时才创建它们。但它并未实现 Map 接口,而且它的 Entry 的 key 是继承 WeakReference 的,也没有看到 HashMap 中的 next,所以不存在链表了。

image.png

假设业务代码中使用完 ThreadLocal,ThreadLocal Ref 被回收了;ThreadLocalMap 中的 Key 强引用了 ThreadLocal,造成 ThreadLocal 被强引用而无法回收。

如果 ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣==内存泄露==。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完 ThreadLocal ⽅法后最好⼿动调⽤ remove() ⽅法。

因为不清楚这个 value 除了 map 的引用还是否还存在其他引用,如果不存在其他引用,当GC的时候就会直接将这个 value 干掉了,而此时我们的 ThreadLocal 还处于使用期间,就会造成 value 为 null 的错误。

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取ThreadLocalMap对象
    if (map != null) // 校验对象是否为空
        map.set(this, value); // 不为空set
    else
        createMap(t, value); // 为空创建一个map对象
}
复制代码

一个线程可以有多个 TreadLocal 来存放不同类型的对象的,但是它们都将放到当前线程的 ThreadLocalMap 里。ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是 ThreadLocal 对象调⽤ set ⽅法设置的值。

ThreadLocalMap 在存储的时候会给每一个 ThreadLocal 对象一个threadLocalHashCode,在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中:①如果当前位置i是空的,就初始化一个 Entry 对象放在位置i上; ② 如果位置i不为空,如果这个 Entry 对象的 key 正好是即将设置的 key,那么就刷新 Entry 中的 value; ③如果位置i的不为空,而且 key 不等于 Entry,那就找下一个空位置,直到为空为止。

在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置,set 和 get 如果冲突严重的话,效率还是很低的。

共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问 ThreadLocal 的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么我就通过init()方法把父线程的inheritThreadLocals给当前线程的inheritThreadLocals

为什么不把ThreadLocalMap定义在Thread类?

将 ThreadLocalMap 定义在 Thread 类内部看起来更符合逻辑,但是 ThreadLocalMap 并不需要 Thread 对象来操作,所以定义在 Thread 类内只会增加一些不必要的开销。定义在 ThreadLocal 类中的原因是 ThreadLocal 类负责 ThreadLocalMap 的创建,仅当线程中设置第一个 ThreadLocal 时,才为当前线程创建 ThreadLocalMap,之后所有其他 ThreadLocal 变量将使用一个 ThreadLocalMap。

总的来说就是,ThreadLocalMap 不是必需品,定义在 Thread 中增加了成本,定义在 ThreadLocal 中按需创建。

应用场景

Spring实现事务隔离级别的源码,采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理 connection 对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。


之前我们上线后发现部分用户的日期居然不对了,当时我们使用SimpleDataFormatparse()方法,内部有一个Calendar对象,调用SimpleDataFormatparse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

其实要解决这个问题很简单,让每个线程都 new 一个自己的 SimpleDataFormat就好了,所以当时我们使用了线程池加上 ThreadLocal 包装SimpleDataFormat,再调用 initialValue 让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改