实践背景:
在系统的门户页面,需要获取多个其他系统接口的数据。因为涉及到用户的体验,渲染页面要求是越快越好,在优化前端、后台的代码前提下,想要提高响应速度那就只好用并发同时请求来提高获取数据的速度。因为不涉及到数据的修改只是单纯的读而且也不要求顺序,暂时不需要我们手写锁只需要JVM提供的就可以了,刚好最近在看方腾飞老师的《Java并发编程的艺术》,在这里推荐一下,是一本很好的书。回到正文,业务场景介绍完了,然后就是技术选型,找到一个最合适业务场景的方式来实现它。
先来介绍一下常用的几类线程池:
线程池 | 使用场景 |
---|---|
newFixedThreadPool | 固定线程数,无界队列,适用于任务数量不均匀的场景,对内存压力不敏感,但系统负载比较敏感的场景。 |
newCachedThreadPool | 无线程数,适用于要求低延迟的短期任务场景 |
newSingleThreadExecutor | 单个线程的固定线程池,适用于保证异步执行顺序的场景。 |
newScheduledThreadPool | 适用于定期执行任务场景,支持固定频率和固定延时 |
newWorkStealingPool | 使用FockJsonPool,多任务队列的固定并行度,适合任务执行时长不均匀的场景。 |
结合我们的业务需求,我们可以使用newFixedThreadPool和newCachedThreadPool,接下来就是实现。
项目使用SpringBoot,环境搭建就不介绍了。
1.封装一个http工具类,用于调用其他系统的API。
package concurrent.base.until;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
/**
因为都是POST请求,本地测试这里用get来测试代码是没问题的。
*/
@Slf4j
public class HttpRemoteUtil {
public static String getHttpRequest(String url) throws IOException {
//创建一个HttpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
// HttpPost post = new HttpPost(url);
HttpGet get = new HttpGet(url);
//这个list用于存放自己要传递的参数,比如说一些认证信息,你懂得。
// List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
// UrlEncodedFormEntity urlEncodedFormEntity = null;
try {
//把需要传递的参数交给这个entity
// urlEncodedFormEntity = new UrlEncodedFormEntity(params, "UTF-8");
//将实体放入http请求中
// post.setEntity(urlEncodedFormEntity);
CloseableHttpResponse response = httpClient.execute(get);
//这个返回值其实就是验证一下,请求是否成功,如果是200的话
int statusCode = response.getStatusLine().getStatusCode();//获取返回的状态值
HttpEntity entity = response.getEntity();
String jsonString = EntityUtils.toString(entity, "UTF-8");
// JSONObject result = JSONObject.parseObject(jsonString);
return jsonString;
} catch (Exception e) {
log.error("[service-synchronizeData]--- error ---, [Exception]= " + e.toString() + "");
} finally {
httpClient.close();
}
return null;
}
public static void main(String[] args) {
try {
String result = getHttpRequest("http://www.baidu.com");
System.out.println(result);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
2.要获取接口的返回结果,所以要用到callable反之就用runable。
package concurrent.base.until;
import java.util.concurrent.Callable;
import com.alibaba.fastjson.JSONObject;
/**
* 线程执行者,我们的数据将在这个类的构造函数里面执行,这个类自带了回调函数。当执行结果返回时会通过它自带的回调将请求结果反馈给Future。
* 事实上我们用到了future,来感知请求是成功还是失败,并且要操作返回的数据,futrue和callable是组合使用的。
*/
public class ThreadHandlerRequest implements Callable<JSONObject>{
private JSONObject requestParams;
public ThreadHandlerRequest(JSONObject paramter) {
this.requestParams = paramter;
}
@Override
public JSONObject call() throws Exception {
// TODO Auto-generated method stub
try {
String htmlJson = HttpRemoteUtil.getHttpRequest(this.requestParams.getString("requestUrl"));
System.out.println(htmlJson);
} catch (Exception e) {
// TODO: handle exception
}
return this.requestParams;
}
}
3.创建一个interface,供demo使用。
public interface ConcurrentReqService {
public Map<String, Object> getUserInfo();
}
4.核心代码来了,我实现了两套,第一种是用future+newFixedThreadPool,第二种使用newcCacheThreadPool+countDownLatch,原因就是想测试一下这两种方式实现的效果哪个更好一点。
第一种方式:
package concurrent.service.Impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import concurrent.base.until.ThreadHandlerRequest;
import concurrent.service.ConcurrentReqService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcurrentReqServiceImpl implements ConcurrentReqService{
//百度
private String baiduUrl = "http://www.baidu.com";
//新浪
private String sinaUrl = "http://www.sina.com";
//爱奇艺
private String iqiyiUrl = "http://www.iqiyi.com";
@Override
public Map<String, Object> getUserInfo() throws InterruptedException {
//组合线程请求参数
JSONArray result = new JSONArray();
JSONObject baidu = new JSONObject();
baidu.put("requestUrl", baiduUrl);
JSONObject sina = new JSONObject();
sina.put("requestUrl", sinaUrl);
JSONObject iqiyi = new JSONObject();
iqiyi.put("requestUrl", iqiyiUrl);
result.add(baidu);
result.add(sina);
result.add(iqiyi);
ExecutorService execPool = Executors.newFixedThreadPool(result.size());
// List<Future<JSONObject>> futures = new ArrayList<Future<JSONObject>>();
List<ThreadHandlerRequest> list = new ArrayList<ThreadHandlerRequest>();
for(int i =0; i < result.size(); i++) {
JSONObject singleobje=result.getJSONObject(i);
list.add(new ThreadHandlerRequest(singleobje));
}
//下面这个逻辑,保证异步任务都可以完成。并且保存上一个异步任务执行的结果
// for (int i =0; i < result.size(); i++) {
//
// JSONObject singleobje=result.getJSONObject(i);
// //申请单个线程执行类
// ThreadHandlerRequest call =new ThreadHandlerRequest(singleobje);
// //提交单个线程
// Future< JSONObject> future = execPool.submit(call);
// //将每个线程放入线程集合, 这里如果任何一个线程的执行结果没有回调,线程都会自动堵塞
// futures.add(future);
//
// }
System.out.println("主线程发起异步任务请求");
long startTime = System.currentTimeMillis();
List<Future<JSONObject>> futures2 = execPool.invokeAll(list);
System.out.println("调用API耗时 : " + (System.currentTimeMillis() - startTime) / 1000);
for(Future<JSONObject> future : futures2) {
try {
JSONObject json = future.get();
System.out.println("--------------------------------" + json);
//业务逻辑,省略
} catch (Exception e) {
log.error("remote Api failed : {}", e.getCause().getMessage());
}
}
long endTime = System.currentTimeMillis();
System.out.println("耗时 : " + (endTime - startTime) / 1000);
//关闭线程池
execPool.shutdown();
return null;
}
public static void main(String[] args) throws InterruptedException {
ConcurrentReqServiceImpl con = new ConcurrentReqServiceImpl();
con.getUserInfo();
}
}
这里介绍一下线程池执行的四个函数(参考JDK8文档):
- submit:提交一个可运行的任务执行,返回任务的执行结果future。
- invokeAny:执行给定的任务,返回成功完成的结果(即,不抛出异常),且任务是一个一个执行的。
- invokeAll:执行给的任务,参数是List,任务是同时异步执行的,返回任务的状态和结果。
- execute:execute提交的方式只能提交一个Runnable的对象,且该方法的返回值是void,也即是提交后如果线程运行后,和主线程就脱离了关系了,当然可以设置一些变量来获取到线程的运行结果。并且当线程的执行过程中抛出了异常通常来说主线程也无法获取到异常的信息的,只有通过ThreadFactory主动设置线程的异常处理类才能感知到提交的线程中的异常信息。
第二种方式:
package concurrent.service.Impl;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import concurrent.base.until.ThreadHandlerRequest;
import concurrent.service.ConcurrentReqService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcurrentReqService2Impl implements ConcurrentReqService{
//百度
private String baiduUrl = "http://www.baidu.com";
//新浪
private String sinaUrl = "http://www.sina.com";
//爱奇艺
private String iqiyiUrl = "http://www.iqiyi.com";
//请求数
private static int clientTotal = 5000;
@Override
public Map<String, Object> getUserInfo() throws InterruptedException {
//newFixedThreadPool 固定线程数,无界队列,适用于任务数量不均匀的场景,对内存压力不敏感,但系统负载比较敏感的场景。
//newCachedThreadPool 无线程数,适用于要求低延迟的短期任务场景
//newSingleThreadExecutor 单个线程的固定线程池,适用于保证异步执行顺序的场景。
//newScheduledThreadPool 适用于定期执行任务场景,支持固定频率和固定延时
//newWorkStealingPool 使用FockJsonPool,多任务队列的固定并行度,适合任务执行时长不均匀的场景。
System.out.println("主线程开始启动----------------------");
//组合线程请求参数
JSONArray result = new JSONArray();
JSONObject baidu = new JSONObject();
baidu.put("requestUrl", baiduUrl );
JSONObject sina = new JSONObject();
sina.put("requestUrl", sinaUrl );
JSONObject iqiyi = new JSONObject();
iqiyi.put("requestUrl", iqiyiUrl );
result.add(baidu);
result.add(sina);
result.add(iqiyi);
//这里使用newCachedThreadPool
ExecutorService exec = Executors.newCachedThreadPool();
CountDownLatch count = new CountDownLatch(result.size());
//ThreadHandlerRequest request = null;
try {
long start = System.currentTimeMillis();
for(int i = 0; i < result.size(); i++) {
ThreadHandlerRequest request = new ThreadHandlerRequest(result.getJSONObject(i));
exec.execute(() -> {
try {
request.call();
//业务逻辑省略
} catch (Exception e) {
log.error("Remote api failed : {}", e.getCause().getMessage());
}finally {
count.countDown();
}
});
}
count.await();
System.out.println("耗时:" + (System.currentTimeMillis() - start)/1000);
exec.shutdown();
} catch (Exception e) {
// TODO: handle exception
}
return null;
}
public static void main(String[] args) throws InterruptedException {
ConcurrentReqService2Impl service2Impl = new ConcurrentReqService2Impl();
service2Impl.getUserInfo();
}
}
上面说到execute方法是没有返回值的,但是我还想拿到结果,就使用了callable,为了能够拿到最终的处理结果,使用了countDownLatch来让主线程wait,其他线程每执行完就减一,当全部完成以后我也就拿到了最终的结果,其实我们还可以使用semaphore来控制并发量,但是业务不需要~~~,这里的countDownLatch的参数是固定的,它是个固定计数器,当我们想使用动态的计数器,就可以考虑使用CyclicBarrier,就当做知识的扩展吧。
结果和分析:
从最终的结果和逻辑上面来看,使用Future更方便一些,它可以保存任务的执行结果和异常状态信息,减少了开发量就不需要单独新创建变量来存储结果和捕获异常信息,另外一种方式也有它的优势,对于线程的调度和状态有着更好的把控,和业务线有点脱节。单纯的只是为了完成这个功能,我会使用Future,如果是你,你会怎么选择呢?
文章内容尽量使用官方文档,希望可以用更接地气并且准确的话来转述,文章目的在于总结和分享知识,如果有不准确的地方还请指出。