案例分析:并行计算让代码“飞”起来
案例分析:并行计算让代码“飞”起来
多线程编程是Java开发中必备的技能,尤其是在面试和实际工作中。本文将通过一个并行获取数据的案例,深入讲解多线程在业务场景中的应用,以及在开发过程中需要注意的关键事项。
并行获取数据
考虑以下场景:有一个用户数据接口,要求在50ms内返回数据。该接口需要从20多个其他接口汇总数据,而每个接口的最小耗时为20ms。如果串行处理,总耗时将远超50ms。因此,唯一可行的解决方案是采用并行处理,通过多线程同时获取计算结果,最后进行结果拼接。
幸运的是,Java提供了丰富的并发工具类,可以简化这类场景的开发。其中,CountDownLatch
是一个非常适合此类场景的工具。它本质上是一个计数器,可以初始化为与执行任务相同的数量。当一个任务执行完成时,计数器的值减1,直到计数器值达到0时,表示完成了所有任务,在await
上等待的线程就可以继续执行。
下面是一个专门为这个场景封装的工具类:
public class ParallelFetcher {
final long timeout;
final CountDownLatch latch;
final ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 1,
TimeUnit.HOURS, new ArrayBlockingQueue<>(100));
public ParallelFetcher(int jobSize, long timeoutMill) {
latch = new CountDownLatch(jobSize);
timeout = timeoutMill;
}
public void submitJob(Runnable runnable) {
executor.execute(() -> {
runnable.run();
latch.countDown();
});
}
public void await() {
try {
this.latch.await(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
public void dispose() {
this.executor.shutdown();
}
}
使用这个工具类的示例如下:
public static void main(String[] args) {
final String userid = "123";
final SlowInterfaceMock mock = new SlowInterfaceMock();
ParallelFetcher fetcher = new ParallelFetcher(20, 50);
final Map<String, String> result = new HashMap<>();
fetcher.submitJob(() -> result.put("method0", mock.method0(userid)));
fetcher.submitJob(() -> result.put("method1", mock.method1(userid)));
fetcher.submitJob(() -> result.put("method2", mock.method2(userid)));
fetcher.submitJob(() -> result.put("method3", mock.method3(userid)));
fetcher.submitJob(() -> result.put("method4", mock.method4(userid)));
fetcher.submitJob(() -> result.put("method5", mock.method5(userid)));
fetcher.submitJob(() -> result.put("method6", mock.method6(userid)));
fetcher.submitJob(() -> result.put("method7", mock.method7(userid)));
fetcher.submitJob(() -> result.put("method8", mock.method8(userid)));
fetcher.submitJob(() -> result.put("method9", mock.method9(userid)));
fetcher.submitJob(() -> result.put("method10", mock.method10(userid)));
fetcher.submitJob(() -> result.put("method11", mock.method11(userid)));
fetcher.submitJob(() -> result.put("method12", mock.method12(userid)));
fetcher.submitJob(() -> result.put("method13", mock.method13(userid)));
fetcher.submitJob(() -> result.put("method14", mock.method14(userid)));
fetcher.submitJob(() -> result.put("method15", mock.method15(userid)));
fetcher.submitJob(() -> result.put("method16", mock.method16(userid)));
fetcher.submitJob(() -> result.put("method17", mock.method17(userid)));
fetcher.submitJob(() -> result.put("method18", mock.method18(userid)));
fetcher.submitJob(() -> result.put("method19", mock.method19(userid)));
fetcher.await();
System.out.println(fetcher.latch);
System.out.println(result.size());
System.out.println(result);
fetcher.dispose();
}
线程池的设置与优化
在上述代码中,线程池的设置是一个关键点。线程池的参数设置需要根据具体场景进行调整。例如,对于I/O密集型任务,线程数应该等于I/O任务的数量;而对于计算密集型任务,线程数应该等于CPU的数量。
从池化对象原理看线程池
线程池的构造方法包含多个参数,其中workQueue
和handler
是两个重要的参数。workQueue
用于存储等待执行的任务,而handler
则定义了当线程池饱和时的拒绝策略。
在SpringBoot中使用异步
在SpringBoot中,可以通过@EnableAsync
和@Async
注解来实现异步任务。同时,建议自定义线程池以避免资源使用不可控的问题。
多线程资源盘点
线程安全的类
在多线程环境中,使用线程安全的类是非常重要的。例如,ConcurrentHashMap
相对于HashMap
具有更好的线程安全性。此外,ThreadLocal
可以用于解决线程安全问题,例如在处理日期格式化时。
线程的同步方式
Java提供了多种线程同步方式,包括synchronized
关键字、ReentrantLock
、volatile
关键字等。每种方式都有其适用场景和优缺点。
FastThreadLocal
Netty为了优化性能,创建了一个名为FastThreadLocal
的结构。它通过改进底层数据结构来提高效率,并对缓存行进行了优化。
多线程使用中常见的问题
在多线程开发中,常见的问题包括线程池配置不当、异常处理不完善等。面试中,面试官经常会询问开发者在多线程使用中遇到的问题,以此来评估其实际应用能力。
异步的本质
异步编程通过将耗时操作转移到后台线程运行,从而减少对主业务的阻塞。它主要优化的是那些阻塞性的等待,能够起到转移冲突、优化请求响应的作用。