流的重定向
记得学C++的时候就接触到了重定向这个概念,比如说下面的cmd
命令:
ipconfig > D:/out.txt
在cmd
命令窗口里执行ipconfig
命令后,本机IP的信息会显示在当前的命令窗口里。但是我们使用了流的重定向,即添加了 >
符号,于是把输出流定向到了out.txt
文件里,而控制台没有输出任何的信息。
再比如说C++里的输入和输出:
std::cin >> n;
std::cout << n;
这个>>
符号可以说是非常的形象了,指向谁就流向谁。在cmd
里>>
代表追加模式,具体可以参考文件的打开方式 fopen("a.txt","a")
。我们再来一个例子:
java Main < test.in > test.out
# 即设定了输入流,也设置了输出流的方向
当然上面的例子只是在命令模式下的,不太适用于我们当前的需求,我们需要的是代码的形式。各种语言都有它设置流的方法:
比如说C语言的:
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
Java的:
static void System.setIn(InputStream in);
static void System.setOut(OutputStream out);
并发需求
单个Judger肯定是不能满足需求的,当多个用户同时提交请求的时候,如果我们是一个一个去执行那真的是太慢了。假设我是用户,提交代码后等待超过4秒后,如果还没有结果的话,我肯定会受不了(当然特殊情况例外)。这时候我们就需要到并发执行了,分别为多个Judger的并发,以及多个测试用例的并发。
但是问题又来了:并发的时候大家用的是同一个输入、输出流。这如果不给区分开来,那肯定会出问题!
ThreadLocal
既然每个线程都需要一个单独的输入、输出流,那每开一条线程就单独new一个流?那肯定是不行的,这样管理起来太麻烦了。
很明显,ThreadLocal
就很适合我们的需求:为每个线程单独管理他们的专有数据。
不懂 ThreadLocal
? 没关系,让我们看一下官方文档:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其
get
或
set
方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
ThreadLocal
实例通常是类中的
private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。…
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且
ThreadLocal
实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
ThreadLocal
底层大概用的是一个 Map
来存储数据,其中的key
就是一个线程对象。而且又使用了虚引用,线程被销毁后会自动帮我们回收垃圾,多好啊~
大概的流程如下图所示,我们主要在读取数据的时候下点手脚。好了,让我们编写自己的流:
/**
* 一个用于多线程的输入流,每个线程都有自己的输入流,互不干扰<p>
* 内部使用 ThreadLocal
* @author yly
*/
public class InputStreamLocal extends InputStream{
private volatile ThreadLocal<InputStream> stream = new ThreadLocal<InputStream>();
/**
* 从输入流中读取一个byte的数据
*/
@Override
public int read() throws IOException {
return this.stream.get().read();
}
/**
* 设置当前线程的输入流
* @param in 输入流
*/
public void setStream(InputStream in) {
this.stream.set(in);
}
}
/**
* 一个基于ThreadLocal的多线程输出流<p>
* 每个线程都有一个属于自己的输出流,互不干扰
* @author yly
*
*/
public class OutputStreamLocal extends OutputStream{
/**
* 一般来说输出流是不需要设置的,我们只关心输出的结果,所以要设置一个默认的流供我们存储数据
* OutputStream 只是个抽象类,不提供存储,也不能获取内容。
* 故使用ByteArrayOutputStream,它内部封装了一个 byte[] buf 用于存储数据
*/
private volatile ThreadLocal<ByteArrayOutputStream> stream = new ThreadLocal<ByteArrayOutputStream>() {
/**
* 当调用ThreadLocal的get方法获取内容时,如果还没有对该值set过
* 那么它会调用initialValue方法来获取一个默认的值,该方法默认返回null
*/
@Override
protected ByteArrayOutputStream initialValue() {
return new ByteArrayOutputStream();
};
};
/**
* 往当前线程的输出流中写入一个byte的数据
*/
@Override
public void write(int b) throws IOException {
this.stream.get().write(b);;
}
/**
* 获取当前输出流中的数据,并重置当前的输出流
* @return byte数组
*/
public byte[] getData() {
ByteArrayOutputStream outputStream = this.stream.get();
byte[] res = outputStream.toByteArray();
outputStream.reset();
return res;
}
}
看了一下ByteArrayOutputStream
的底层源码,真的是深有感触:里面内置了一个byte
数组,执行写操作的时候就对该byte数组进行写入数据。那岂不是可以创建一个既可以读又可以写的InOrOutStream
?一看 InputStream
和OutputStream
都是定义为抽象类,Java的单继承瞬间让我的想法破灭了。。。
杂项
核心的代码完成得差不多了,让我们捋一下接下来的需求
- 给我一个字符串类型的测试用例,然后转成输入流
- 计算时间的消耗,以及内存的消耗
- 使用线程池来对线程的管理
- 答案的对拍
- Judger单独设为一个系统(中间件),只要通过Socket来通信
因为使用的是 JavaCompiler
来编译Java代码,这个局限性实在是太大了,我们当前只能测评Java的代码,而不能兼并C/C++, Python。如果使用 Docker
的虚拟环境的话就相对简单得多了。这个拓展我们后面再考虑吧。。。太难了,我都没用过Docker
~