java实现代码在线编译器-从零开发(三)Web并发环境下的线程安全

并发环境下会出现什么问题?

上一篇已经测试过,单个请求是能正常执行并且返回的。但是,系统部署在公网上往往不可能一个人使用,因此必须经过并发测试,不求多规范,至少简单的并发测试也是要进行的。
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)。此时已经出现问题了。

Spring默认单例(后发现与本次无关)

用的的应该都知道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覆盖了。

如何解决

  1. 加锁(synchronized)

并发问题最新想到的肯定是锁机制,使得临界资源操作同步化来保证线程安全。使用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;
        }
    }
}
  1. 不同文件名

同步代码(上锁)性能较低,还有其他效率更好的方法。
很容易想到随机给文件命名,但由于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"

主要改动就是目录加上了时间戳,配置写死改成了从配置文件读取,执行结束新启动一个线程删除两个文件及文件夹。

下一步

本篇解决线程安全的问题,但是线程数量依旧没有进行限制以及其他问题,但总算解决了一个问题。后续还会补充完善代码…………

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值