赞
踩
上一篇已经测试过,单个请求是能正常执行并且返回的。但是,系统部署在公网上往往不可能一个人使用,因此必须经过并发测试,不求多规范,至少简单的并发测试也是要进行的。
Apifox图形化界面测试十分简单,还能添加变量。如下所示,简单点,两个线程循环两遍。
修改测试代码,Thread.sleep(1000)模拟测试程序需多耗时一秒。编辑一个自增变量(Apifox文档一使用说明,每次请求id+1)
{
"code": "public class Main {\n public static void main(String[] args) throws Exception{ Thread.sleep(1000); System.out.println(\"hello world----自增id {% mock 'increment', 1 %} \");}}"
}
下面给出我的测试结果:
线程1第一轮: { "code": "public class Main {\n public static void main(String[] args) throws Exception{ Thread.sleep(1000); System.out.println(\"hello world----自增id 3 \");}}" } { "error": 0, "reason": "运行成功", "stderr": "", "stdout": "hello world----自增id 4 \n" } 线程1第二轮: { "code": "public class Main {\n public static void main(String[] args) throws Exception{ Thread.sleep(1000); System.out.println(\"hello world----自增id 5 \");}}" } { "error": 0, "reason": "运行成功", "stderr": "", "stdout": "hello world----自增id 6 \n" } 线程2第一轮: { "code": "public class Main {\n public static void main(String[] args) throws Exception{ Thread.sleep(1000); System.out.println(\"hello world----自增id 4 \");}}" } { "error": 0, "reason": "运行成功", "stderr": "", "stdout": "hello world----自增id 4 \n" } 线程2第二轮: { "code": "public class Main {\n public static void main(String[] args) throws Exception{ Thread.sleep(1000); System.out.println(\"hello world----自增id 6 \");}}" } { "error": 0, "reason": "运行成功", "stderr": "", "stdout": "hello world----自增id 6 \n" }
可知四次网络请求,分别发送了3,4,5,6。而执行结果为(4,4)(6,6)。此时已经出现问题了。
用的的应该都知道SpringMVC(SpringBoot还是有的MVC)的Controller,默认是单例的,可以进行如下测试:
先修改一下TestController的代码,输出对象且直接返回:
@RequestMapping(value = "/run")
public String run(@RequestBody JSONObject json){
System.out.println(this+"---"+service);
/*
Answer answer=service.run(json.getString("code"));
if(answer==null)
return "{\"error\":\"IO错误\"}";
else
return JSONObject.toJSONString(answer);
*/
return "";
}
然后重启!!!重启!!!重启!!!再用软件发送HTTP请求查看控制台输出
显而易见,每次网络请求都是同一个Controller对象,同一个对象自动注入的依赖Service必然也是一样的。这也说明了Spring确实默认为单例的。
但是,没有什么关系。。。。。。因为并没有用到这两个对象的成员变量,无线程安全问题。
随着代码走到TestService,就可以很容易发现这段代码:
String DIR="d:/javaTest/";
String javaFile=DIR+"Main.java";
String javaClass="Main";
//编译命令
String compileCmd=String.format("javac -encoding utf8 %s -d %s",javaFile,DIR);
//运行命令
String runningCmd=String.format("java -classpath %s %s", DIR, javaClass);
//将代码写入到定义路径下特定的java源文件中
hhh,前面简单实现为例方便,路径以及文件名都是写死的。而编译以及运行都是耗时操作,简单画图说明
id=3的请求到达,写入了磁盘还运行得出结果返回就可能被id=4的请求覆盖掉了java源文件,所以才会出现两个线程都返回4的情况。第二轮同理,5被6覆盖了。
并发问题最新想到的肯定是锁机制,使得临界资源操作同步化来保证线程安全。使用synchronized即可,更细粒度的锁考虑过了不可行,这里耗时的三步:写源文件、编译读源文件写class、执行读class,可以看做一个事务,都有涉及到两个临界资源的读操作。因此读写锁也优化不了多大。。。
已开10线程五轮进行测试,并未发现问题。
@Service public class TestService { private Boolean javaLock=true; public Answer run(String code){ String DIR="d:/javaTest/"; String javaFile=DIR+"Main.java"; String javaClass="Main"; //编译命令 String compileCmd=String.format("javac -encoding utf8 %s -d %s",javaFile,DIR); //运行命令 String runningCmd=String.format("java -classpath %s %s", DIR, javaClass); synchronized (javaLock) { //将代码写入到定义路径下特定的java源文件中 FileWriter writer; try { File dir = new File(DIR); if (!dir.exists()) { dir.mkdir(); } writer = new FileWriter(javaFile); writer.write(code); writer.close(); } catch (IOException e) { e.printStackTrace(); return null; } //编译源文件为class文件 Answer answer = ExecUtil.run(compileCmd, false, true); System.out.print(answer.getStderr()); //若编译成功即可开始运行 if (answer.getError() == 0) { answer = ExecUtil.run(runningCmd, true, true); if (answer.getError() == 0) answer.setReason(Answer.Success); else answer.setReason(Answer.RuntimeError); System.out.print(answer.getStdout()); } else answer.setReason(Answer.Error); return answer; } } }
同步代码(上锁)性能较低,还有其他效率更好的方法。
很容易想到随机给文件命名,但由于java的特性,public class必须和文件名保持一致,故无法随机给文件命名,如果是C/C++就可以随机命名实现。文件名不行?那就改目录呗,只要每个线程操作的不是一个文件就不存在线程安全问题了。我想到的是可以在目录上加一个时间戳。。。
package com.deng.service; import com.deng.bean.Answer; import com.deng.util.ExecUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Date; @Service public class TestService { @Value("${java_file.work_dir}") private String work_dir; @Value("${java_file.compile}") private String compile; @Value("${java_file.running}") private String running; public Answer run(String code){ String time="/"+ new Date().getTime()+"/"; String DIR=work_dir+time; String javaFile=DIR+"Main.java"; String javaClass="Main"; //编译命令 String compileCmd=String.format(compile,javaFile,DIR); //运行命令 String runningCmd=String.format(running, DIR, javaClass); //将代码写入到定义路径下特定的java源文件中 FileWriter writer; File dir=new File(DIR); try { dir.mkdir(); writer = new FileWriter(javaFile); writer.write(code); writer.close(); } catch (IOException e) { e.printStackTrace(); return null; } //编译源文件为class文件 Answer answer= ExecUtil.run(compileCmd,false,true); System.out.print(answer.getStderr()); //若编译成功即可开始运行 if(answer.getError()==0) { answer = ExecUtil.run(runningCmd, true, true); if(answer.getError()==0) answer.setReason(Answer.Success); else answer.setReason(Answer.RuntimeError); System.out.print(answer.getStdout()); } else answer.setReason(Answer.Error); //删除两个文件+文件夹,若想要复查代码可以不删,此处需求是在线运行不是判题(虽然保存也是存数据库),故直接删除 new Del(javaFile,DIR+javaClass+".class",dir).start(); return answer; } class Del extends Thread{ private File javaFile; private File classFile; private File dir; Del(String javaFile,String classFile,File dir){ this.classFile=new File(classFile); this.javaFile=new File(javaFile); this.dir=dir; } @Override public void run() { javaFile.delete(); classFile.delete(); dir.delete(); } } }
application.yml
server:
servlet:
context-path: /
port: 8080
java_file:
work_dir: "d:/javaTest/"
compile: "javac -encoding utf8 %s -d %s"
running: "java -classpath %s %s"
主要改动就是目录加上了时间戳,配置写死改成了从配置文件读取,执行结束新启动一个线程删除两个文件及文件夹。
本篇解决线程安全的问题,但是线程数量依旧没有进行限制以及其他问题,但总算解决了一个问题。后续还会补充完善代码…………
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。