一、为什么设计多线程
举个例子说明多线程的作用。比如有100个人去食堂打菜,如果只有一个窗口,那么所有人都需要在这个窗口进行排队,一个打完才能排到下一个,如果每个人打菜需要1分钟,这100个人打完菜总耗时就是100分钟,这就类似程序中的单线程。如果有5个打菜窗口,那么就可以每次5个人几乎同时进行打菜,相当于把100个人分成5个队,这样打菜的时间就减少了5倍,这100个人打完菜总耗时差不多为20分钟。这就类似于程序中使用了多线程。所以,使用多线程对用户体验的提升毋庸置疑。
类似,物业软件中也有很多场景可以使用多线程。如多线程远程开门(同时开整个小区所有的门)、多线程下载卡、多线程下载设备参数、多线程删除卡等。
二、多线程的设计思路
本文旨在设计一个通用型的多线程Http客户端。既然是通用,肯定是要兼容各种数据类型,而json字符串恰好能承担这个重任。另一方面,几乎所有的业务执行都需要得到执行结果反馈,故多线程的设计还需要考虑业务上的阻塞。这篇多线程程序阻塞核心算法如下:
public List<String> send() {
for(String data : jsonArray) {
threadPool.execute(new MultiThread(url, data));
}
threadPool.shutdown();
while (!threadPool.isTerminated()) {
}
return jsonArrayResponse;
}
从上述程序可看出,多线程的创建及执行使用了线程池做管理,而ExecutorService中的shutdown和isTerminated方法结合起来就可以实现多线程阻塞。
其次,对于某些多线程业务,可能还需要依赖外部资源。如物业软件的门禁记录转发,由于设备门禁记录和抓拍的图片数据是异步上传的,通常记录会先到,抓拍的图片会延迟一两秒才上报完成。而转发给第三方的门禁数据中需要包含抓拍图片,那怎么办呢?本文同样提供了解决办法,如下:
(1)转发门禁记录时,使用LockUtil的lock方法(key为图片路径)将转发线程锁住。
(2)图片数据上报到物业中心时,先使用LockUtil的setParam方法把图片数据设置到缓存中,再使用LockUtil的unlock方法(key为图片路径)将图片解锁。
(3)解锁后的门禁记录线程,使用LockUtil的getParma方法把图片数据从缓存中读出来,并添加到记录的属性中,就可了转发一条完整的门禁记录到第三方了。
核心算法如下:
@Override
public void run() {
super.run();
// 若线程有锁,此处处理线程锁中的参数。
if (hasLock) {
// 使用LockUtil的lock方法(key为图片路径)将转发线程锁住
LockUtil.getInstance().lock(lockKey, 20);
// 使用LockUtil的getParma方法把图片数据从缓存中读出来
String param = (String)LockUtil.getInstance().getParam(lockKey);
// 把参数添加到记录的属性中
if (StringUtils.isNotBlank(param)) {
JSONObject obj = JSON.parseObject(data);
obj.put(lockParamKey, param);
data = JSON.toJSONString(obj);
}
}
String res = HttpRequest.post(url).body(data).execute().body();
jsonArrayResponse.add(res);
}
上述设计方法看似已经没什么问题,其实不然,由于应答数据是从多线程中逐条添加的,并且使用了ArrayList,而ArrayList是线程不安全的(ArrayList线程为什么不安全,请移步到https://blog.csdn.net/u012859681/article/details/78206494)。要解决ArrayList的线程安全问题其实很简单,只需要将多线程中的ArrayList改成如下写法即可:
List<String> jsonArrayResponse = Collections.synchronizedList(new ArrayList<>());
三、程序摘要
package com.leelen.ehome.multithread;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.leelen.ehome.utils.LockUtil;
import com.xiaoleilu.hutool.http.HttpRequest;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Http多线程客户端
*
* @Author:Jack
*/
public class HttpMultithreadClient {
/**
* json字符串数组,即需要发送的数据。
*/
private List<String> jsonArray;
/**
* 发送目的地址:url
*/
private String url;
/**
* 是否对线程加锁,默认否
*/
private boolean hasLock = false;
/**
* key,线程锁秘钥
*/
private String lockKey;
/**
* 从锁中读出的参数名称
*/
private String lockParamKey;
/**
* json格式应答数组数据
*/
private List<String> jsonArrayResponse = Collections.synchronizedList(new ArrayList<>());
/**
* 线程池
*/
private ExecutorService threadPool = Executors.newFixedThreadPool(10);
/**
*
* @param jsonArray json格式数组(数据体)
* @param url 发送地址url
* @param hasLock 是否加锁,若加锁则继续处理后两个参数
* @param lockKey 若加锁则该参数必填,否则可为null
* @param lockParamKey 若加锁则该参数必填,否则可为null
*/
public HttpMultithreadClient(List<String> jsonArray, String url, boolean hasLock, String lockKey, String lockParamKey) {
this.jsonArray = jsonArray;
this.url = url;
this.hasLock = hasLock;
this.lockKey = lockKey;
this.lockParamKey = lockParamKey;
}
/**
* 多线程发送
* @return
*/
public List<String> send() {
for(String data : jsonArray) {
threadPool.execute(new MultiThread(url, data));
}
threadPool.shutdown();
while (!threadPool.isTerminated()) {
// 业务上需要等待执行结果,故此处多线程创建完毕后,需要阻塞等待执行结果的加载。
}
return jsonArrayResponse;
}
/**
* 多线程实现类
*/
class MultiThread extends Thread {
/**
* 发送目的地址:url
*/
private String url;
/**
* 数据
*/
private String data;
public MultiThread(String url, String data) {
this.url = url;
this.data = data;
}
@Override
public void run() {
super.run();
// 若线程有锁,此处处理线程锁中的参数。
if (hasLock) {
LockUtil.getInstance().lock(lockKey, 20);
String param = (String)LockUtil.getInstance().getParam(lockKey);
if (StringUtils.isNotBlank(param)) {
JSONObject obj = JSON.parseObject(data);
obj.put(lockParamKey, param);
data = JSON.toJSONString(obj);
}
}
String res = HttpRequest.post(url).body(data).execute().body();
jsonArrayResponse.add(res);
}
}
}