实现优先使用运行线程及调整线程数大小的线程池

当前在JDK中默认使用的线程池 ThreadPoolExecutor,在具体使用场景中,有以下几个缺点

  1. core线程一般不会timeOut
  2. 新任务提交时,如果工作线程数小于 coreSize,会自动先创建线程,即使当前工作线程已经空闲,这样会造成空闲线程浪费
  3. 设置的maxSize参数只有在队列满之后,才会生效,而默认情况下容器队列会很大(比如1000)

如一个coreSize为10,maxSize为100,队列长度为1000的线程池,在运行一段时间之后的效果会是以下2个效果:

  1. 系统空闲时,线程池中始终保持10个线程不变,有一部分线程在执行任务,另一部分线程一直wait中(即使设置allowCoreThreadTimeOut)
  2. 系统繁忙时,线程池中线程仍然为10个,但队列中有还没有执行的任务(不超过1000),存在任务堆积现象

本文将描述一下简单版本的线程池,参考于 Tomcat ThreadPoolExecutor, 实现以下3个目标

  1. 新任务提交时,如果有空闲线程,直接让空闲线程执行任务,而非创建新线程
  2. 如果coreSize满了,并且线程数没有超过maxSize,则优先创建线程,而不是放入队列
  3. 其它规则与ThreadPoolExecutor一致,如 timeOut机制
继续阅读“实现优先使用运行线程及调整线程数大小的线程池”

一种在线程池中透传或继承ThreadLocal信息的方法

在实际的业务代码中,经常会使用到 ThreadLocal 用于跨业务代码 来获取在上游设置的值。比如,在spring mvc中 spring web mvc 中通过 RequestContextHolder 设置 HttpServletRequest,业务代码则可以在 controller 或者是 service中 通过 RequestContextHolder#getRequestAttributes 获取相应的对象. 但这种方法有一个限制,即 setValue 和 getValue 的代码必须在同一个线程内. 当然,这也是属于通过 ThreadLocal 来避免竞争的一种手法.

针对之前已经可以工作的代码,如果将相应的业务代码 迁移至一个新的线程池中运行,即封装为 1个 runnable 对象,那么相应的代码即不能正确地工作了。

如下参考所示

ThreadLocal<String> local = new ThreadLocal<>();
local.set("value1");

//打印 获取->value1
System.out.println("获取->" + local.get());

Runnable runnable = () -> {
    System.out.println("获取->" + local.get());
};
//打印 获取->null
new Thread(runnable).start();

上面的 sout 可以是任意1个业务上的 method 调用。仅仅是将 method调用 转由新线程来运行,相应的业务逻辑即不能正常工作。 至于这里将调用转由新线程来运行,可以有很多的场景。如 支持 timedout 调用(利用future.get(timed))。

本文通过反射调用读取当前Thread的信息,将值注入到新线程中的threadLocal中,以达到透传threadLocal的目的。不需要修改任何业务代码,也不需要使用InheritableThreadLocal(此类也并不用于当前场景)

主要的实现基于以下步骤

  • 提前提取当前线程ThreadLocal变量值
  • 执行时复制至新线程中
  • 执行结束之后删除未变化值

本文中的代码均基于反射调用,需要打开相应的 Accessible 属性.

继续阅读“一种在线程池中透传或继承ThreadLocal信息的方法”

在基于spring体系的业务中正确地关闭线程池

在业务代码中,特别是基于spring体系的代码中,均会使用线程池进行一些操作,比如异步处理消息,定时任务,以及一些需要与当前业务分离开的操作等。常规情况下,使用spring体系的TaskExecutor或者是自己定义ExecutorService,均可以正常地完成相应的操作。不论是定义一个spring bean,或者是使用 static Thread工具类均是能满足条件。

但是,如果需要正常地关闭spring容器时,这些线程池就不一定能够按照预期地关闭了。结果就是,当使用代码 context.close() 时,期望进程会正常地退出,但实际上进程并不会退出掉.原因就在于这些线程池中还在运行的线程。
本文描述了在基于spring boot的项目中,如何正确地配置线程池,以保证线程池能够正确的在整个spring容器周期内运行,并且在容器正常关闭时能够一并退出掉.

  • 定义bean时添加destroyMethod方法或相应生命周期方法
  • 设置线程池中线程为daemon
  • 为每个线程池正确地命名及使用ThreadFactory
  • 丢弃不再需要的周期性任务
  • 监听ContextClosedEvent,触发额外操作
继续阅读“在基于spring体系的业务中正确地关闭线程池”

使用guava Striped中的lock导致线程死锁的问题分析

在最近的开发调试当中,一个多线程的项目出现了线程死锁的问题。按照正常的解决思路,上监控工具yourkit或者自己打印出线程信息,最终的显示信息均为lock对象先拿到了自己的锁,然后去拿对方的锁,现状是这样的。但与一些正常的项目相比,按照正常的执行情况,是不可能出现死锁的。

项目使用了程序锁来保证并发情况下,一些操作只能由单个来执行,如一些不允许多次运行的场景。所谓程序锁,即通过由锁池获取相应的lock对象,然后进行lock操作,在相应的方法体内进行相应的控制。
项目中使用基于guava的striped来获取不同的锁,本来原来的代码是自己封装实现,但考虑到guava已经提供,因此直接使用了,相应的伪代码如下所示:

        Striped<Lock> lockStriped = Striped.lazyWeakLock(1024);

        Lock lock = lockStriped.get("动态生成的临界资源标识符");
        lock.lock();
        try{
            //执行业务操作
        } finally {
            lock.unlock();
        }

以上的代码因为足够的通用,因此被封装为一个LockAdvice,以实现通用的锁方法操作,只需要在需要加锁的方法上加上Lockable注解,就可以达到所要的效果。出现死锁的2个线程表现如下:

//线程1
加锁获取资源类型1
加锁获取资源类型2

//线程2
加锁获取资源类型1
加锁获取类型类型2

从上面的线程操作来看,加锁的顺序是一样的,释放锁的顺序也一致,因此理论上不会出现死锁。为保证锁切面的简单实现,上面的锁资源类型1 被表现为一个字符串,即通过不同的字符串来进行描述。也就是说,在上面的现象中,会有4个字符串进行锁的key进行操作。

问题场景被发现了,并且这个问题肯定会产生,多线程运行一段时间之后就会发生。查找问题产生的原因却很漫长。不知道为什么,使用jstack却没有打印出每个线程在哪一个环节持有哪个锁(是不是本身就没有),通过jstack的信息表示,相应的线程肯定持有对方的锁,而对方也在等级已方的锁,并且相应的锁的发生点也确定是在相应的锁切面准备执行的时候。剩下的问题就是看是否是相应的锁生成器striped本身就返回了相同的锁,如果由锁生成器本身就返回相同的锁,那么就肯定有问题了。

继续阅读“使用guava Striped中的lock导致线程死锁的问题分析”

使用spring typeConverter造成的数据错乱(多线程环境)

在线上环境碰到一个问题,经由数据库查询并进行数据处理转换之后的数据在界面显示时随机出现数据错位。由于该问题不可重现(重新清除缓存并操作一次问题即解决),并且由于缓存的存在(缓存了错误的数据),导致此问题严重并且很难查找。

业务场景描述如下:

数据库查询数据->一次处理->类型转换->二次处理->界面显示

由于每个步骤都涉及到很多代码,因此在处理时通过在不同的点设定潜在出错点,并通过判断数据变化进行log。尝试在本地环境重现此问题。经过一次偶然的场景,发现一个本该是8位数字的字符串在转换过程中报错,由此找出了真正的问题。

//报错的业务代码 假定数据为 20141111
str = s.substring(0,4) + "/" + s.substring(4,6) + "/" + s.substring(6,8)
//报错异常 indexOutofBound,即字符串不足8位...

经过断点,加层层回溯,终于发现数据在经过一次类型转换之后被修改了。转换代码如下所示:

private static TypeConverter typeConverter = new SimpleTypeConverter();
        public <T> T convertValue(Object value) {
            return (T)typeConverter.convertIfNecessary(value, Integer.class);
        }

初看起来,这个方法调用并没有什么问题,即将数据值转换为整数类型.重点的问题在于在这个方法(由spring提供)的内部实现并不是线程安全的。附API说明:

public interface TypeConverter
Interface that defines type conversion methods. Typically (but not necessarily) implemented in conjunction with the PropertyEditorRegistry interface.
Note: Since TypeConverter implementations are typically based on PropertyEditors which aren't thread-safe, TypeConverters themselves are not to be considered as thread-safe either.

typeConverter内部会使用propertyEditor来进行类型转换。这种转换在spring3.0之后已不再推荐使用。见 使用线程安全的spring类型转换器ConversionService VS TypeConverter

解决方法也简单,将其更换为conversionService即可(为什么不自己写?不想重复发明轮子)。spring3.0之后,为conversionService内置了常见类型之间的转换,相当于整个类型转换子框架被完全重写了,重写的转换是状态无关的,即线程安全。至于原来的typeConverter及propertyEditor,忘掉它吧,遗留的东西。

附:jdk自带的simpleDateFormat也存在相同的问题,用这个类需要小心处理线程问题。

guava中的FinalizableReferenceQueue解析

1 引用队列的监控和回调
当创建一个引用队列时,我们需要对这个队列进行监控,即开启一个新的线程来循环判断此队列信息,并从中获取相应的数据信息。在guava中,也是通过创建一个线程然后在循环中进行判断,如下所示(类com.google.common.base.internal.Finalizer):

    Thread thread = new Thread(finalizer);
    thread.setName(Finalizer.class.getName());
    thread.setDaemon(true);

    thread.start();

即创建一个以finalizer runnable对象的线程,然后启动之,为避免阻止进程结束,采用了后台线程的模式。那在这个finalizer中,其运行如下所示:

while (true) {
      try {
        if (!cleanUp(queue.remove())) {
          break;
        }
      } catch (InterruptedException e) { /* ignore */ }
    }

即不断地获取队列中的数据,然后调用cleanUp方法.而在cleanUp方法中,其实就是调用相应对象的回调方法,即当一个对象已经被gc时的回调。

在guava中,从队列中移除的是一个是reference对象。在java体系中,并没有在reference对象中定义相应的回调方法,因此guava为jdk的reference增加了新的定义接口,称之为FinalizableReference。在这个接口中,即定义了一个相应的回调函数,如下所示:

/** 当引用被gc之后,此方法会被触发调用 */
  void finalizeReferent();

因为增加了新的接口,因此我们自己来使用的话,就即要继承于jdk的weakReference,又要实现此接口。为方便,guava定义了3套与jdk兼容的引用对象。即FinalizableWeakReference,FinalizableSoftReference和FinalizablePhantomReference。可以理解为就是一个对jdk原类型的一个适配。

继续阅读“guava中的FinalizableReferenceQueue解析”