1、问:假如在一个spring mvc的框架下,当收到一个用户请求,当前请求需要调用5个 rmi接口,为了提高性能,需要使用并发的方式实现,可以怎么做?
答:并发在java中可以开启多线程的方式实现。通过new Thread可以创建一个线程执行,在线程中执行rmi逻辑。
2、问:确定是要用new Thread的方式么?会存在怎样的问题?
答:线程的创建和关闭都是一个比较消耗资源的操作,在java中类似的操作,比如长连接,数据库连接等都会用到池化技术。所以可以使用线程池的方式实现。jdk的concurrent包中提供了几个内置的线程池给我们使用,比如:
1. Executors.newCachedThreadPool
2. Executors.newFixedThreadPool
前者为无限大小的线程池(其实是int的最大值),后者为限定大小的线程池。根据不同的场景我们使用不同的线程池。
3、问:这些线程池的原理是什么,假如使用newFixExecutorService,在很大并发量冲击的情况下,会出现什么问题?
答:查看newFixedThreadPool的源码或者《java 并发编程实践》一书中可以看到,newFixedThreadPool方法的源码是:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool是构造一个ThreadPoolExecutor放回。在其构造函数中指定初始大小,线程池最大值,是否保持活跃,所使用的等待队列等。由于该方法执行了线程池的最大大小,当请求数大于当前线程池最大值时,后面的请求将会放到等待队列中进行等待。在newFixedThreadPool是使用阻塞无界队列实现的。所以当请求无限大的时候,会存在内存溢出的影响。
不仅仅如此,假如当前业务依赖的服务出现了性能问题,导致rt下降得十分厉害,客户端请求达到超时时间之后就会返回,可是假如当前业务处理方法中对于依赖服务的超时时间大于客户端超时时间,那么在客户端超时之后,对应的线程资源并没有能够得到释放,这样就会无法正常处理后续的客户请求。
4、问:嗯嗯,没错,那么有什么办法可以解决这种问题?
答:线程池的设计除了前面提到的一些最大大小,等待队列之外,还有一个很重要的属性就是拒绝策略:RejectedExecutionHandler。jdk中内置了下面4中拒绝策略:
1. DiscardPolicy
2. AbortPolicy
3. DiscardOldestPolicy
4. CallerRunsPolicy
拒绝策略表示当请求队列满了之后,对后续的请求采用何种拒绝方式。我们可以使用ThreadPoolExecutor来创建自己的线程池,定义线程池大小,使用有界队列(指定LinkBlockingQueue的大小),使用CallerRunsPolicy为拒绝策略。表示当请求过载时使用当前的客户端线程执行业务功能。这样也可以在一定的程度上降低客户端带来的压力
5、问:那么对于这个线程池的初始化和销毁,你会怎么做?
答:在spring中提供了很多种方式可以进行bean的初始化和容器事件扩展。比如:
1. 使用bean的init方法
2. 监听Spring context的event消息获取spring 的启动完成
3. 实现InitialtionBean接口
4. 实现LifecycleProcessor接口
这里可以实现InitialtionBean在bean的初始化完成之后,初始化线程池(为什么不在构造函数或者init方法中使用,这个在后续的文章会进行说明)。而对于线程池的销毁,可以使用: 1. 指定destroy 方法 2. 实现DisposableBean接口 3. 实现LifecycleProcessor接口
最好是选择使用LifecycleProcessor的方式,因为在容器关闭时,会先调用LifecycleProcessor接口后再进行bean的销毁。而Spring对于Bean的destory顺序是没有保证的(会保证按照初始化顺序的逆序执行),所以就有可能存在的风险是线程池还没有销毁,业务处理所依赖的bean就已经close掉。
6、问:回到第一个问题,使用多线程的方式执行5个rmi接口,但是线程都是异步的,怎么返回结果给客户端呢?
答:使用线程池执行rmi调用时,确实是一个异步操作。对于客户端的相应是需要5个rmi结果都处理完之后才知道最终的结果,所以不能在提交任务到线程池就直接返回。
这种情况下可以使用jdk的CountdownLatch进行等待。在所有任务提交到线程池之后,执行闭锁的等待,设置等待时间。当超过时间都还没有执行完,闭锁的await方法会返回false,否则会返回true。执行超时并不代表线程就会结束掉,线程结束需要线程的任务执行完或者是使用终端的方式种植。