Java并发请求多个API的实践

实践背景:
          在系统的门户页面,需要获取多个其他系统接口的数据。因为涉及到用户的体验,渲染页面要求是越快越好,在优化前端、后台的代码前提下,想要提高响应速度那就只好用并发同时请求来提高获取数据的速度。因为不涉及到数据的修改只是单纯的读而且也不要求顺序,暂时不需要我们手写锁只需要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,如果是你,你会怎么选择呢?

文章内容尽量使用官方文档,希望可以用更接地气并且准确的话来转述,文章目的在于总结和分享知识,如果有不准确的地方还请指出。

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!对方的API请求可以使用Java并发库来实现。最常用的库是Java的Executor框架和线程池。 您可以创建一个线程池,将API请求任务提交给线程池执行。这样可以并发地发送多个请求并同时处理响应。 下面是一个简单的示例代码: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class APIClient { private static final int THREAD_POOL_SIZE = 10; // 线程池大小根据实际情况设置 public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE); // 提交API请求任务 Future<String> response1 = executor.submit(() -> sendApiRequest("API_URL_1")); Future<String> response2 = executor.submit(() -> sendApiRequest("API_URL_2")); // 处理响应 try { String result1 = response1.get(); String result2 = response2.get(); // 对响应结果进行处理 System.out.println("Response from API 1: " + result1); System.out.println("Response from API 2: " + result2); } catch (Exception e) { e.printStackTrace(); } // 关闭线程池 executor.shutdown(); } private static String sendApiRequest(String apiUrl) { // 发送API请求并返回响应结果的逻辑 return "Response from " + apiUrl; // 这里只是示例,实际需要根据API请求方式来实现发送请求的逻辑 } } ``` 在上述示例中,我们使用了大小为10的线程池来发送两个API请求。您可以根据实际情况调整线程池的大小。每个API请求都被封装成一个带返回结果的任务,并提交给线程池执行。通过调用`get()`方法,我们可以获取到API请求的响应结果并进行处理。 希望以上信息对您有所帮助!如果您有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值