mark 和 reset
在说明问题之前先根据源码说明下 mark 和 reset 方法的用处:
mark
/**
* @param readlimit the maximum limit of bytes that can be read before
* the mark position becomes invalid.
* @see java.io.BufferedInputStream#reset()
*/
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = pos;
}
@param readlimit 在标记失效前允许读取的最大字节数,readlimit 的含义也就是在 mark 位置之后,可以读取 readlimit 长度的字节数,之后标记点将不再生效,配合 reset 方法查看更清晰。
reset
/**
* <p>
* If <code>markpos</code> is <code>-1</code>
* (no mark has been set or the mark has been invalidated), an <code>IOException</code>
* is thrown. Otherwise, <code>pos</code> is set equal to <code>markpos</code>.
* @see java.io.BufferedInputStream#mark(int)
*/
public synchronized void reset() throws IOException {
getBufIfOpen(); // Cause exception if closed
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}
当 marpos(在 mark 方法中赋值,即最近的标记位置) 是空时(未设置标记或者标记已经失效),直接抛出异常。否则,将该流读取的起始位置设置为之前的标记位置。
总结
其实综合两个方法来看,mark 在输入流中标记当前位置,方便 reset 之后能够从标记点位置开始重新获取到相同的信息。
问题描述
在使用 BufferdInputStream 用到 mark( int readlimit) 和 reset() 方法, 但是 mark 中的参数无论设置多少,无论在设置标记之后读取多少字节,标记位置都不会失效,调用 reset 之后都将从最近的一次标记位置读取。即:我设置了 readlimit 为 0,然后读取了1024 个字节,markpos 仍然有效, reset 之后仍然可以实现从标记点重新读取信息。
实例及源码解析
问题复现
待码
//创建一个 7 个字节的字符串
String textTxt = "ABCDEFG";
//转换为 BufferedInputStream
InputStream iStream = new ByteArrayInputStream(textTxt.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bis = new BufferedInputStream(iStream);
// MRAK 在起始位置就打下标记
bis.mark(6);
//读取全部字节 已经超过了设置的 readLimit
int rd = bis.read();
while(rd!=-1){
System.out.println("First Time: "+ (char)rd);
rd = bis.read();
}
// RESET ,回到之前的标记位置
bis.reset();
int rd2 = bis.read();
System.out.println("Second Time:"+(char)rd2);
打印结果
First Time: A
First Time: B
First Time: C
First Time: D
First Time: E
First Time: F
First Time: G
Second Time:A
源码分析
从最后打印出来的结果可以看出,reset 之后,输入流从位置 0 重新读取信息,表示 mark 标记仍然有效。
但是在上面 mark 和 reset 的源码中除了对标记属性和当前位置的赋值外,没有看到任何其他对标记位置处理的逻辑。
所以我们要寻找其他的”可疑“代码,我们在代码中只用到了 mark 、reset 和 read,所以”嫌疑“理所当然的落在了 read 头上,查看 read 的代码
read()源码
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
//当前位置大于等于字节数总量,返回-1
if (pos >= count)
return -1;
}
//读取下一个字节
return getBufIfOpen()[pos++] & 0xff;
}
明确问题点
我们查看代码中的 fill 方法
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) /* no room left in buffer */
if (markpos > 0) { /* can throw away early part of the buffer */
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else { /* grow buffer */
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
// Can't replace buf if there was an async close.
// Note: This would need to be changed if fill()
// is ever made accessible to multiple threads.
// But for now, the only way CAS can fail is via close.
// assert buf == null;
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
在 fill 方法中处理到了 markpos,那应该就是它没跑了,接下来就看一下它是怎么处理的。
正常的输入流读取方式
我们先看一下涉及 markpos 的逻辑片段,其实就是输入流读取字节的逻辑
//获取缓存buffer 承接从 InputStream 中读取到的字节
byte[] buffer = getBufIfOpen();
//没有设置过标记 那么就从起始位置开始读取字节
if (markpos < 0)
pos = 0;
else if (pos >= buffer.length) /* 缓存buffer空间不够了 */
...
//count赋值
count = pos;
//从pos 开始向后读取字节,读取长度为 buffer.length - pos
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
//最后实际读取了 n 个字节(因为可能剩余的字节不够 buffer.length - pos 个长度)
if (n > 0)
//对count重新赋值
count = n + pos;
后续在 read 逻辑中,如果当前位置没有超过 buffer 中字节总数的大小,将直接从 buffer 中读取字节
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
//当前位置大于等于字节数总量,返回-1
if (pos >= count)
return -1;
}
//读取下一个字节
return getBufIfOpen()[pos++] & 0xff;
}
涉及 markpos 的处理逻辑
//1.当读取的位置大于等于缓存buffer的长度时,进入以下逻辑
else if (pos >= buffer.length)
if (markpos > 0) {
//2.1 存在标记点 且标记位置大于 0
//会从标记位置开始将缓存buffer中字节的信息后copy下来,在放到缓存buffer中
//并将起始位置修改为标记位置,标记位置修改为0,当前位置向前推 markpos 个位置
//总结来说 就是把 markpos 位置之前的字节都抛弃了
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
//2.2 markpos = 0 的情况 即在一开始就设置了标记位置,或者经过了2.1之后markpos 修改为0 并且buffer的长度大于等于marklimit
//将 markpos 和 pos 重置 即标记失效
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
//2.3 buffer大小超过最大值 OOM
throw new OutOfMemoryError("Required array size too large");
} else {
//2.4 markpos = 0 的情况 当buffer的长度小于marklimit时 进行扩容
//先找到 pos * 2 和 MAX_BUFFER_SIZE 中的最小值
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
//在判断其与markLimit的最小值
if (nsz > marklimit)
nsz = marklimit;
//取最小值扩容
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
//count赋值
count = pos;
//从pos 开始向后读取字节,读取长度为 buffer.length - pos
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
//最后实际读取了 n 个字节(因为可能剩余的字节不够 buffer.length - pos 个长度)
if (n > 0)
//对count重新赋值
count = n + pos;
问题处理
根据上面的源码分析可以看到,对于标记失效的关键逻辑在于:
//当前读取的位置大于等于buffer的长度时才会走这里的逻辑
else if (pos >= buffer.length)
...
else if (buffer.length >= marklimit) {
//在上面的逻辑中可以看到,当buffer.length<marklimit 时,buffer会扩容至大小等于marklimit
//如果 buffer.length >= marklimit ==> pos >= marklimit
//即读取内容超出了设置的标记有效的极限,将 markpos 和 pos 重置 即标记失效
markpos = -1;
pos = 0;
}...
标记失效实现的条件是:
- 当前输入流读取的位置大于等于 buffer 的长度
- buffer 的长度大于等于 marklimit
我们在初始化时并没有给 BufferdInputStream 设置 buffer 的大小,所以将使用默认大小
private static int DEFAULT_BUFFER_SIZE = 8192;
1. 所以我们此时之只能在输入流读取了至少 8192 个字节之后,标记点才会失效。
2. 或者我们设置 reallimit 大于8192 并读取超过 reallimit 个字节,标记也会失效。
3. 我们可以修改 buffer 大小让他尽快进入失效逻辑,修改代码
//创建一个 7 个字节的字符串
String textTxt = "ABCDEFG";
//转换为 BufferedInputStream
InputStream iStream = new ByteArrayInputStream(textTxt.getBytes(StandardCharsets.UTF_8));
//关键的修改 在这里设置 buffer的大小为5
BufferedInputStream bis = new BufferedInputStream(iStream,5);
// MRAK 在起始位置就打下标记 读取 6 个字节后 标记失效 buffer 会自动扩容至 6
bis.mark(6);
//读取7个字节 在读取第7个字节时 标记点会失效
int j =0;
int rd = bis.read();
while(rd!=-1 && j<6){
System.out.println("First Time: "+ (char)rd);
rd = bis.read();
j++;
}
// RESET ,回到之前的标记位置(抛出异常)
bis.reset();
int rd2 = bis.read();
System.out.println("Second Time:"+(char)rd2);
此时在 reset 方法执行时将会抛出异常,标记失效:
First Time: A
First Time: B
First Time: C
First Time: D
First Time: E
First Time: F
First Time: G
Exception in thread "main" java.io.IOException: Resetting to invalid mark
at java.io.BufferedInputStream.reset(BufferedInputStream.java:448)
at com.dm.java.demo.Test.main(Test.java:35)