1、概念
1.1、用来做什么
1.2、意图梳理
1.3、使用场景
经典网关场景,查询多个系统数据,由于是接口调用,存在阻塞性,所以并不适合下边这种情况,但本教程会涉及到这个场景的实现来达到了解ForkJoinPool的使用,通过这个场景可以更清楚了解ForkJoinPool的使用:
通常的使用场景下边1.5会列出
1.4、实现思路
1.5、适用
2、代码
2.1、应用场景
一个方法中调用多个微服务获取数据:
上边这样写的问题是很大的,接口响应总时间大概为调用的各个微服务接口时间之和(还不包括本接口的其他逻辑处理),所以这样效率是非常低的。我们可以采用多线程应用来做这个事情。先来了解forkjoinpool,它本质上还是一个线程池,默认的线程数量为cpu的核数,可以通过调用Executeors来获取,也可以直接用return 后边的代码进行创建:
forkjoinpool的使用方法与我们平时的线程池类似,也是用submit方法,不过除了可以穿runable和callable参数,多了可以传forkjointask参数的方法,由于这个类是一个抽象类,我们经常继承下边两个子抽象类做具体实现:
2.2、场景解决方法
前两个顺便提供了这个场景的常用方法,最后一个是为了介绍ForkJoinPool的使用才写的,实际中这样用到的并不多
2.2.1、利用FutureTask解决
这里顺便为了了解FutureTask的大致原理,自制了一个简单的futuretask,如不需要了解FutureTask原理可直接跳过:
import java.util.concurrent.*;
import java.util.concurrent.locks.LockSupport;
// 我们想一想,这个功能怎么实现
// (jdk本质,就是利用一些底层API,为开发人员提供便利)
public class NeteaseFutureTask<T> implements Runnable, Future { // 获取 线程异步执行结果 的方式
Callable<T> callable; // 业务逻辑在callable里面
T result = null;
volatile String state = "NEW"; // task执行状态
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();// 定义一个存储等待者的集合
public NeteaseFutureTask(Callable<T> callable) {
this.callable = callable;
}
@Override
public void run() {
try {
result = callable.call();
} catch (Exception e) {
e.printStackTrace();
// result = exception
} finally {
state = "END";
}
// 唤醒等待者
Thread waiter = waiters.poll();
while (waiter != null) {
LockSupport.unpark(waiter);
waiter = waiters.poll(); // 继续取出队列中的等待者
}
}
// 返回结果,
@Override
public T get() {
if ("END".equals(state)) {
return result;
}
waiters.offer(Thread.currentThread()); // 加入到等待队列,线程不继续往下执行
while (!"END".equals(state)) {
LockSupport.park(); // 线程通信的知识点
}
// 如果没有结束,那么调用get方法的线程,就应该进入等待
return result;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return false;
}
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return null;
}
}
然后我们利用上边这个自制的futuretask进行解决问题,当然这个自制的futuretask也可以换成jdk中的FutureTask,只不过上边做了简单的实现仅仅来满足我们的需求:
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.*;
/**
* 串行调用http接口
*/
@Service
public class UserServiceFutureTask {
ExecutorService executorService = Executors.newCachedThreadPool();
@Autowired
private RestTemplate restTemplate;
/**
* 查询多个系统的数据,合并返回
*/
public Object getUserInfo(String userId) throws ExecutionException, InterruptedException {
// 其他例子, 查数据库的多个表数据,分多次查询
// 原味爱好
// Future < > Callable
// 1 和runnable一样的业务定义. 但是本质上是有区别的: 返回值 异常 call run.
Callable<JSONObject> callable = new Callable<JSONObject>() {
@Override
public JSONObject call() throws Exception {
// 1. 先从调用获取用户基础信息的http接口
long userinfoTime = System.currentTimeMillis();
String value = restTemplate.getForObject("http://www.tony.com/userinfo-api/get?userId=" + userId, String.class);
JSONObject userInfo = JSONObject.parseObject(value);
System.out.println("userinfo-api用户基本信息接口调用时间为" + (System.currentTimeMillis() - userinfoTime));
return userInfo;
}
};
// 通过多线程运行callable
NeteaseFutureTask<JSONObject> userInfoFutureTask = new NeteaseFutureTask<>(callable);
new Thread(userInfoFutureTask).start();
NeteaseFutureTask<JSONObject> intergralInfoTask = new NeteaseFutureTask(() -> {
// 2. 再调用获取用户积分信息的接口
long integralApiTime = System.currentTimeMillis();
String intergral = restTemplate.getForObject("http://www.tony.com/integral-api/get?userId=" + userId,
String.class);
JSONObject intergralInfo = JSONObject.parseObject(intergral);
System.out.println("integral-api积分接口调用时间为" + (System.currentTimeMillis() - integralApiTime));
return intergralInfo;
});
new Thread(intergralInfoTask).start();
// 3. 合并为一个json对象
JSONObject result = new JSONObject();
result.putAll(userInfoFutureTask.get()); // 会等待任务执行结束
result.putAll(intergralInfoTask.get());
return result;
}
}
FutureTask的应用,核心是不要有依赖关系:
2.2.2、利用CountDownLatch解决
当然为了代码的复用性,下边的接口调用可以封装成一个工具类传入url就行了。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 调用http接口
*/
@Service
public class UserServiceCountLatch {
ExecutorService executorService = Executors.newCachedThreadPool();
@Autowired
private RestTemplate restTemplate;
/**
* 查询多个系统的数据,合并返回
*/
public Object getUserInfo(String userId) throws InterruptedException {
CountDownLatch count = new CountDownLatch(2);
ArrayList<JSONObject> values = new ArrayList<>();
// 你可以封装成一个 提交URL 就能自动多线程调用的 工具
executorService.submit(() -> {
// 1. 先从调用获取用户基础信息的http接口
long userinfoTime = System.currentTimeMillis();
String value = restTemplate.getForObject("http://www.tony.com/userinfo-api/get?userId=" + userId, String.class);
JSONObject userInfo = JSONObject.parseObject(value);
System.out.println("userinfo-api用户基本信息接口调用时间为" + (System.currentTimeMillis() - userinfoTime));
values.add(userInfo);
count.countDown();
});
executorService.submit(() -> {
// 2. 再调用获取用户积分信息的接口
long integralApiTime = System.currentTimeMillis();
String intergral = restTemplate.getForObject("http://www.tony.com/integral-api/get?userId=" + userId,
String.class);
JSONObject intergralInfo = JSONObject.parseObject(intergral);
System.out.println("integral-api积分接口调用时间为" + (System.currentTimeMillis() - integralApiTime));
values.add(intergralInfo);
count.countDown();
});
count.await();// 等待计数器归零
// 3. 合并为一个json对象
JSONObject result = new JSONObject();
for (JSONObject value : values) {
result.putAll(value);
}
return result;
}
}
2.2.3、利用ForkJoinPool来解决(不建议用这个解决接口调用)
为什么说不是最优解,因为forkjoinpool实现复杂,并且接口调用是阻塞的任务,所以根据1.5的概念,最好不要用这个解决。
下边这个代码是可以复用的,因为上边代码需要根据具体调用的接口数量来改变代码,而下边直接将所有接口都放到了urls中,由RecursiveTask的实现类中重写compute方法来进行了递归操作,然后将所有结果合并,提高代码复用。
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.concurrent.*;
/**
* 并行调用http接口
*/
@Service
public class UserServiceForkJoin {
// 本质是一个线程池,默认的线程数量:CPU的核数
ForkJoinPool forkJoinPool = new ForkJoinPool(10, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
@Autowired
private RestTemplate restTemplate;
/**
* 查询多个系统的数据,合并返回
*/
public Object getUserInfo(String userId) throws ExecutionException, InterruptedException {
// 其他例子, 查数据库的多个表数据,分多次查询
// fork/join
// forkJoinPool.submit()
ArrayList<String> urls = new ArrayList<>();
urls.add("http://www.tony.com/userinfo-api/get?userId=" + userId);
urls.add("http://www.tony.com/integral-api/get?userId=" + userId);
HttpJsonRequest httpJsonRequest = new HttpJsonRequest(restTemplate, urls, 0, urls.size() - 1);
ForkJoinTask<JSONObject> forkJoinTask = forkJoinPool.submit(httpJsonRequest);
JSONObject result = forkJoinTask.get();//获取结果,这里的get也是阻塞的
return result;
}
}
// 任务
class HttpJsonRequest extends RecursiveTask<JSONObject> {
RestTemplate restTemplate;
ArrayList<String> urls;
int start;
int end;
HttpJsonRequest(RestTemplate restTemplate, ArrayList<String> urls, int start, int end) {
this.restTemplate = restTemplate;
this.urls = urls;
this.start = start;
this.end = end;
}
// 就是实际去执行的一个方法入口(任务拆分)
@Override
protected JSONObject compute() {
int count = end - start; // 代表当前这个task需要处理多少数据
// 自行根据业务场景去判断是否是大任务,是否需要拆分
if (count == 0) {
String url = urls.get(start);
// TODO 如果只有一个接口调用,立刻调用
long userinfoTime = System.currentTimeMillis();
String response = restTemplate.getForObject(url, String.class);
JSONObject value = JSONObject.parseObject(response);
System.out.println(Thread.currentThread() + " 接口调用完毕" + (System.currentTimeMillis() - userinfoTime) + " #" + url);
return value;
} else { // 如果是多个接口调用,拆分成子任务 7,8, 9,10
System.out.println(Thread.currentThread() + "任务拆分一次");
int x = (start + end) / 2;
HttpJsonRequest httpJsonRequest = new HttpJsonRequest(restTemplate, urls, start, x);// 负责处理哪一部分?
httpJsonRequest.fork();//调用的这个fork方法就是把当前任务再提交到线程池处理队列中,再开线程进行处理提高效率
HttpJsonRequest httpJsonRequest1 = new HttpJsonRequest(restTemplate, urls, x + 1, end);// 负责处理哪一部分?
httpJsonRequest1.fork();
// join获取处理结果
JSONObject result = new JSONObject();
result.putAll(httpJsonRequest.join());
result.putAll(httpJsonRequest1.join());
return result;
}
}
}
运行结果,因为调用了两个接口,所以只需要拆分一次:
3、ForkJoinPool与普通线程池的区别
普通线程池:
ForkJoinPool:
区别就是每个线程都有自己的队列,原理就是下边的图,每个线程执行大task和由大task拆分成的小task,但是问题来了,这样大task和小task不都由同一个线程来串行执行了吗,这里还有一个forkjoinpool的特点,就是工作窃取,如果其他线程的队列里边没有任务的话,会分担其他线程的任务队列,这就是工作窃取,所以这个forkjoinpool实现的源代码也是相当麻烦的,看源码的话会发现jdk中用了五百行去描述它是怎么实现的,目的是提高线程效率。
但也不是说forkjoinpool就是万能的,从上边1.5可以知道,最好不要声明太多的forkjoinpool,适合非阻塞的和内存性的操作,因为一旦阻塞就意味着线程的浪费,所以网络操作,数据库操作,文件操作就最好不要用他,如果要用最好只声明一个线程池,线程数为cpu核数。具体什么情况下适合呢,比如说我们传了1000w个字符串,我们需要知道不同字符串出现的次数,这种纯数据计算类的可以用这个。