遇到一个场景,请求报文进来服务后要从许多渠道收集相关信息,一般情况是串行下一个渠道获取完再到另一个渠道获取。这样一来响应时间普遍偏慢,甚至偶尔出现超时的现象,于是就想用多线程同时从不同渠道获取信息。先上代码,再说遇到的坑
public class KdAplyVO {
public String name;
public String id;
public String code;
public int errMsg = 0;
省略set,get
import com.sun.xml.internal.ws.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
public class TestVolatile {
public static void main(String[] args) {
String code1 = "01";//客户01
String code2 = "02";//客户02
//模拟webService层
Thread thread3 = new Thread() {
@Override
public void run() {
new QuseryMain().query(code1);
}
};
thread3.start();
//模拟webService层层
Thread thread4 = new Thread() {
@Override
public void run() {
new QuseryMain().query(code2);
}
};
thread4.start();
}
}
class QuseryMain {
// private static ThreadLocal<KdAplyVO> threadLocal = new ThreadLocal<>(); 不能用threadlocal,子线程不同步
public KdAplyVO query(String code) {
//需持久化数据
KdAplyVO kdAplyVO = new KdAplyVO();
kdAplyVO.setCode(code);
//模拟数据库获取任务
List<String> taskList = new ArrayList<String>();
taskList.add("setName");
taskList.add("setId");
taskList.add("setId1");
taskList.add("setId2");
taskList.add("setId3");
taskList.add("setId4");
taskList.add("setId5");
taskList.add("setId6");
List<Thread> threadList = new ArrayList<Thread>();
// threadLocal.set(kdAplyVO);不能用threadlocal,子线程不同步
for (String task : taskList) {
QueryBS queryBS = new QueryBS(kdAplyVO, task);
Thread thread = new Thread(queryBS);
thread.start();
threadList.add(thread);
}
//有超时风险,总的超时时间为30秒,每个线程最多等25秒,实际应用注意打日志用于判断哪个线程超时
for (Thread thread : threadList) {
try {
thread.join(25000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主1kdAplyVO = " + kdAplyVO.getName() + "," + kdAplyVO.getId());
//本方法中的KdAplyVO并没有被volatile修饰,不休眠或者不打印的话不会停止循环,意味着变量没有同步到本线程中(原因可能为线程一直循环,虚拟机没空进行同步)
// ,因此需要等线程join后才能保证同步?(测试没有join时会无线循环,添加join后则不会)网上查阅资料并没有明确说join后可以同步,保证同步的话可以考虑使用回调函数或future;
while (true) {
if (null != kdAplyVO.getName() && null != kdAplyVO.getId()) {
// System.out.println("主1kdAplyVO = " + kdAplyVO.getCode() + "," + kdAplyVO.getName() + "," + kdAplyVO.getId());
break;
}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println("主kdAplyVO = " +kdAplyVO.getName()+","+kdAplyVO.getId());
}
//此处可进行持久化
System.out.println("主2kdAplyVO = " + kdAplyVO.getCode() + "," + kdAplyVO.getName() + "," + kdAplyVO.getId()+ "," + kdAplyVO.getErrMsg());
return kdAplyVO;
}
}
class QueryBS implements Runnable {
//volatile 保证线程数据从内存中获取,因此即使QueryBS不是同一个对象,kdAplyVO是同一个对象应该也是能够同步的
public volatile KdAplyVO kdAplyVO;
public String param;
public QueryBS(KdAplyVO kdAplyVO, String param) {
this.kdAplyVO = kdAplyVO;
this.param = param;
}
@Override
public void run() {
if (this.param.equals("setName")) {
if ("01".equals(kdAplyVO.code)) {
//模拟从数据源获取数据花费的时间
// System.out.println("code1休眠前");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println("code1休眠后");
this.kdAplyVO.setName("aliali");
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);
} else {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.kdAplyVO.setName("bilibii");
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);
}
} else if (this.param.equals("setId")) {
if ("01".equals(kdAplyVO.code)) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.kdAplyVO.setId("111111");
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);
} else {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.kdAplyVO.setId("222222");
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);
}
}else{
//KdAplyVO不被volatile修饰时会陷入死循环,修饰时则正常同步,说明即使QueryBS不是同一个对象,但是里面KdAplyVO被volatile修饰也会同步,因为它们都是操作主内存的数据
while(this.kdAplyVO.getErrMsg() == 0){}
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);
}
// System.out.println("后kdAplyVO = " + kdAplyVO.getCode() + "," + kdAplyVO.getName() + "," + kdAplyVO.getId());
}
public KdAplyVO getKdAplyVO() {
return kdAplyVO;
}
public void setKdAplyVO(KdAplyVO kdAplyVO) {
this.kdAplyVO = kdAplyVO;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
首先第一个问题时如何给每个线程分配任务。
一种方法是每个任务单独写一个线程类,在主线程调用时直接判断调用,但是当任务量多时特别繁琐,而且也不容易对存量代码进行改造。
第二种方法就是将所有任务置入一个类统一管理中,如QueryBS类中的run方法,由主线程往子线程中传入参数,然后子线程依据参数来判断。
一开始的方案为共用一个QueryBS对象,主线程参数传入的参数绑定到threadlocal上,然后判断threadlocal上的参数进行判断进行哪个任务。但是并没有找到合适的方案。
网上有类似方案如参数传给QueryBS,再与threadlocal绑定,但我觉得不靠谱,如果是同一个QueryBS对象在绑定之前,第二个请求进来将QueryBS里参数改变就将引起混乱。如果再创建一个QueryBS对象,那就无需与threadlocal绑定了。
还有一种方案是从主线程的InheritableThreadLocal获取,但是一样面临上一种方案的问题,在子线程获取之前有可能下一个线程就进来改变了。
因此最后选择创建多个QueryBS对象的方式来为每个线程分配任务。
第二个问题就是数据同步问题,创建了一个持久化对象kdAplyVO,不能是主线程的成员变量,不能用volatile修饰。可以传入QueryBS对象,在线程对象QueryBS中可以用volatile修饰。
那么就又有两个问题,一个是子线程如何与主线程同步,测试发现由于方法中的KdAplyVO并没有被volatile修饰,不休眠或者不打印的话不会停止循环,意味着变量没有同步到本线程中(原因可能为线程一直循环,虚拟机没空进行同步)。
另一个问题是由于上一个任务分配问题,所以每个线程QueryBS对象是不一致的,那么里面的成员变量kdAplyVO是否还能同步。
第一个问题一开始打算在主线程中threadlocal解决,但是仔细一想,各个线程中的threadlocal压根就是私有不同步的,此方案作废。然后,就考虑用线程join的方式,测试没有join时会无线循环,添加join后则不,但是网上查阅资料并没有明确说join后可以同步,希望哪位大佬可以指教下。另外保证同步的话可以考虑使用回调函数或future;或者可以使用CurrentHashMap来实现同步,晚点验证下
Java线程之异步回调(Callback)
第二个问题的话,由于kdAplyVO被volatile修饰,所以不管QueryBS对象是否一样,都是会去操作内存,从而实现同步。
以下为测试验证
//KdAplyVO不被volatile修饰时会陷入死循环,修饰时则正常同步,说明即使QueryBS不是同一个对象,但是里面KdAplyVO被volatile修饰也会同步,因为它们都是操作主内存的数据
while(this.kdAplyVO.getErrMsg() == 0){}
this.kdAplyVO.setErrMsg(this.kdAplyVO.getErrMsg()+1);