JavaでBufferedReaderのreadLineメソッドを使用して発生した問題(readLineブロック)
16924 ワード
Socket通信のアイテムを作成する際にBufferedReaderを使用し、readLine関数で情報を読み取りますが、readLineを読み取りメッセージのループの外に置いた後、プログラムが次の文を実行できなくなり、GitHub接続に対応します(後でGitHubブログで更新し、GitHubブログを基準にします).
上の6文目を見て、プログラムがここまで実行されると実行できなくなり、androidクライアントでもこのような問題が発生しました.
ここでreadLineをwhileループに配置し,上段のサービス側で発生した後のコードを実行できない問題を解決したが,ここのhandlerではまた問題が発生した.
handlerはこのクラスでグローバル変数であり、メインスレッドから渡された値であり、androidではチャットインタフェースごとに異なるhandlerを設定していたので、whileループが1回実行されると、handler値は前回保存された値であり、不思議ではないでしょうか.この問題は数年前に遭遇しましたが、その時も下層の原理を理解していませんでした.今回はreadLine実装元コードをよく検討しますが、readLineはBufferedReaderの情報を読み取っているので、まずBufferedReaderというクラスを見てみましょう.
BufferedReaderでもInputStreamReaderとInputStreamを呼び出しているので、まずInputStreamとOutputStreamを見てみましょう.
InputStream,OutputStream
InputStreamはバイト入力ストリームのすべてのクラスのスーパークラスであり,一般にFileInputStreamなどのサブクラスを用いる.
OutputStreamはバイト出力ストリームのすべてのクラスのスーパークラスであり、一般的にFileOutputStreamなどのサブクラスを使用します.
以上はInputStreamのソースコードです.JDK(ここでは1.8バージョン)で探してもいいです.私は中の注釈を全部削除しました.InputStreamはCloseableインタフェースを呼び出したのですが、CloseableインタフェースはAutoCloseableクラスを継承しています.底は言わないで、Closeableインタフェースのソースコードを見てもいいです.
このインタフェースには一言、つまりcloseという方法があるので、この方法はストリームを閉じるために使用され、それに関連する任意の方法が解放されます.ストリームが閉じている場合、この方法を呼び出すのは意味がありません.InputStreamがこのインタフェースを呼び出すのもcloseメソッドを実現するためだけです.
次にInputStreamというクラスを見てみましょう.上のソースコードには抽象的なクラスが見えます.中には主にread方法があり、もう一つのskip方法があります.まずread方法を見てみましょう.ここには2つのread方法があります.1つは抽象方法で、もう1つのreadは上のread抽象方法を再ロードし、それを具体的に実現しました.read方法は主に入力ストリームで読み取ったデータを1つの配列に保存し、具体的には一つ一つ分析しません.そしてskipメソッドは、入力ストリームデータのnバイトをスキップまたは破棄する役割を果たすと信じています.もう一つのクラスOutputStreamは先に分析しません.ここでreadを分析します.
次にInputStreamReaderとOutputStreamWriterの2つのクラスを見てみましょう
このInputStreamReaderクラスの役割は、バイトストリームを文字ストリームに変換することであり、InputStreamReaderがReaderクラスを継承していることがわかります.では、Readerクラスがどのように実現されているかを見てみましょう.
ReaderとInputStreamは少し似ていることがわかります.例えば、Closeableというインタフェースも実現されていますし、read方法もあります.read方法には3つの実現方法があり、skip方法もあります.それらの役割も悪くありませんが、ここでのread方法は文字ストリームのデータを読み出しています.さらにskipと前のInputStreamのskipは少し違いますが、ここのskipはロックされています.このskipマルチスレッドの方法はスレッドが安全であることがわかります.あまり言わないで、もう一度過去に戻って上の
InputStreamReader,InputStreamReaderクラスは4回の構造方法をリロードしたほか,2つのread方法,readyとclose方法が残っているが,ここでのreadは親Readerのものであり,上書き,すなわち書き換えが行われているはずである.ここでInputStreamReaderはReaderとInputStreamの間にあり、バイトストリームを文字ストリームに変換するのに適した役割を果たしています.
次にポイントBufferedReaderとBufferedWriterを見てみましょう
以上、分析してようやくポイントが見えてきましたが、BufferedReaderソースコードからBufferedReaderにもreadメソッドとskipメソッドがあり、readLineというメソッドが多くなり、BufferedReaderもReaderクラスを継承していることがわかります.では、readLineというメソッドを具体的に見てみましょう.
中にsynchronized(lock)という言葉があるのが見えますが、このreadLineの方法はロックされていますが、これはポイントではありません.次のbufferLoopを見てください.これはポイントです.bufferLoopの中にfor(;)これはデッドサイクルなので、データの読み取りが終わるたびにブロックされます.では、第1の問題をどのように解決するかというと、第2の問題のようにreadLineをループに配置することで、「または'r」を読み取るたびにreadlineは値を返し、whileループに入り、ループのコードも読み取り後に実行されます.ここでreadlineという方法は、リターン文字または改行文字を読み出すたびに返され、クライアントがメッセージを送信する最後にリターン文字または改行文字を付けないと、readlineは値を返すことができません.2つ目の問題は、handlerを静的変数に設定して解決したのですが、androidクライアントはreadLineがデータを読み出すたびにメインスレッド(UIスレッド)にメッセージを転送し、最初は前回のhandler値を呼び出していたので、後でhandlerを静的に設定して問題が解決したのですが、なぜそうなったのでしょうか.
これはJavaにおけるメモリ割り当てに関し,Javaにおけるメモリは一般にメソッド領域,スタック,JVMメソッドスタック,ローカルメソッドスタックに分けられる.レジスタ(cache)一般的な静的変数はメソッド領域に存在し、newからのオブジェクトはスタックメモリに存在し、handlerがグローバル変数に設定されている場合、この変数の値はnewからのオブジェクトと同様にスタックメモリに存在する.各スレッドは独自のJVMスタックメモリを有し、1つのスレッドは他のスレッドのJVMスタックの変数とメソッドにアクセスできない.ここまで言うと、変数値がスタックメモリに存在し、変数がJVMスタックに存在するのは疑問です.スタック:ローカル変数(ローカル変数の値を含む)、静的変数のポインタ(参照)、メソッド、オブジェクトのポインタ(参照)、グローバル変数のポインタ(参照)、配列のポインタ(参照)など スタック:newから出るオブジェクト(オブジェクトにはグローバル変数ポインタが指す真実値およびその他が含まれる)、配列に格納された値など .メソッド領域:静的変数ポインタ(参照)が指す真実値、クラス名、メソッド情報、プログラムコンパイル後のコードなど Javaクラスでグローバル変数を定義すると、この変数のポインタがスタックに格納され、このグローバル変数が指すメモリブロックがスタックに格納されます.つまり、本当にデータはスタックに格納されています(グローバル変数がオブジェクトに格納されているため)、スタックにはこのデータのアドレスが格納されているだけです.そうすれば、このポインタが他のメモリアドレスを指している場合、前のメモリブロックは二度とアクセスできません.その後、JVMはゴミ回収メカニズムを利用してクリーンアップされます.
JVMは新しく作成したスレッドごとにスタックを割り当て、各javaアプリケーションはJVM実列に対応し、各インスタンスはスタックに対応します.つまりjavaアプリケーションの各スレッドのそれぞれのスタックはjavaアプリケーションというスタックの中にあります.静的変数は一般的にメソッド領域に格納され、メソッド領域もグローバルに共有され、つまりすべてのスレッドが共有されています.もちろんjavaアプリケーションのスタックもグローバル共有であり、JVMスタック、ローカルメソッドスタック、レジスタは非共有である.
したがって,タイトルの問題に戻り,handlerを静的変数に設定すると,グローバル共有となり,すべてのスレッドがアクセスできるようになったので,whileサイクルでreadLineで読み取った情報をhandlerで送信するプライマリスレッド,handlerの値が前のオブジェクトの値であるかという問題を解決した.他は分析を展開しないで、更に展開して私が気を失うことを恐れて、以上はすべて個人の理解で、もし間違いがあれば指摘してください.
public void run() {
try {
String LineString = null;
BufferedReader br = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "UTF-8"));
LineString = br.readLine();
while (LineString!=null) {
System.out.println(LineString);
ChatManager.getChatManager().PublishInf(this, LineString);
}
br.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
上の6文目を見て、プログラムがここまで実行されると実行できなくなり、androidクライアントでもこのような問題が発生しました.
messagebean.setBr(new BufferedReader(new InputStreamReader(socket.getInputStream())));
while ((line = messagebean.getBr().readLine()) != null) {
Message message = new Message();
message.what = msgWhat;
Bundle bundle = new Bundle();
bundle.putString("message", line);
message.setData(bundle);
handler.sendMessage(message);
}
ここでreadLineをwhileループに配置し,上段のサービス側で発生した後のコードを実行できない問題を解決したが,ここのhandlerではまた問題が発生した.
handlerはこのクラスでグローバル変数であり、メインスレッドから渡された値であり、androidではチャットインタフェースごとに異なるhandlerを設定していたので、whileループが1回実行されると、handler値は前回保存された値であり、不思議ではないでしょうか.この問題は数年前に遭遇しましたが、その時も下層の原理を理解していませんでした.今回はreadLine実装元コードをよく検討しますが、readLineはBufferedReaderの情報を読み取っているので、まずBufferedReaderというクラスを見てみましょう.
BufferedReaderでもInputStreamReaderとInputStreamを呼び出しているので、まずInputStreamとOutputStreamを見てみましょう.
InputStream,OutputStream
InputStreamはバイト入力ストリームのすべてのクラスのスーパークラスであり,一般にFileInputStreamなどのサブクラスを用いる.
OutputStreamはバイト出力ストリームのすべてのクラスのスーパークラスであり、一般的にFileOutputStreamなどのサブクラスを使用します.
package java.io;
public abstract class InputStream implements Closeable {
private static final int MAX_SKIP_BUFFER_SIZE = 2048;
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
public long skip(long n) throws IOException {
long remaining = n;
int nr;
if (n <= 0) {
return 0;
}
int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
byte[] skipBuffer = new byte[size];
while (remaining > 0) {
nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
if (nr < 0) {
break;
}
remaining -= nr;
}
return n - remaining;
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
以上はInputStreamのソースコードです.JDK(ここでは1.8バージョン)で探してもいいです.私は中の注釈を全部削除しました.InputStreamはCloseableインタフェースを呼び出したのですが、CloseableインタフェースはAutoCloseableクラスを継承しています.底は言わないで、Closeableインタフェースのソースコードを見てもいいです.
package java.io;
import java.io.IOException;
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
このインタフェースには一言、つまりcloseという方法があるので、この方法はストリームを閉じるために使用され、それに関連する任意の方法が解放されます.ストリームが閉じている場合、この方法を呼び出すのは意味がありません.InputStreamがこのインタフェースを呼び出すのもcloseメソッドを実現するためだけです.
次にInputStreamというクラスを見てみましょう.上のソースコードには抽象的なクラスが見えます.中には主にread方法があり、もう一つのskip方法があります.まずread方法を見てみましょう.ここには2つのread方法があります.1つは抽象方法で、もう1つのreadは上のread抽象方法を再ロードし、それを具体的に実現しました.read方法は主に入力ストリームで読み取ったデータを1つの配列に保存し、具体的には一つ一つ分析しません.そしてskipメソッドは、入力ストリームデータのnバイトをスキップまたは破棄する役割を果たすと信じています.もう一つのクラスOutputStreamは先に分析しません.ここでreadを分析します.
次にInputStreamReaderとOutputStreamWriterの2つのクラスを見てみましょう
package java.io;
import java.nio.charset.Charset;
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
public InputStreamReader(InputStream in, String charsetName)
throws UnsupportedEncodingException
{
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}
public InputStreamReader(InputStream in, Charset cs) {
super(in);
if (cs == null)
throw new NullPointerException("charset");
sd = StreamDecoder.forInputStreamReader(in, this, cs);
}
public InputStreamReader(InputStream in, CharsetDecoder dec) {
super(in);
if (dec == null)
throw new NullPointerException("charset decoder");
sd = StreamDecoder.forInputStreamReader(in, this, dec);
}
public String getEncoding() {
return sd.getEncoding();
}
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
public boolean ready() throws IOException {
return sd.ready();
}
public void close() throws IOException {
sd.close();
}
}
このInputStreamReaderクラスの役割は、バイトストリームを文字ストリームに変換することであり、InputStreamReaderがReaderクラスを継承していることがわかります.では、Readerクラスがどのように実現されているかを見てみましょう.
package java.io;
public abstract class Reader implements Readable, Closeable {
protected Object lock;
protected Reader() {
this.lock = this;
}
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
public int read(java.nio.CharBuffer target) throws IOException {
int len = target.remaining();
char[] cbuf = new char[len];
int n = read(cbuf, 0, len);
if (n > 0)
target.put(cbuf, 0, n);
return n;
}
public int read() throws IOException {
char cb[] = new char[1];
if (read(cb, 0, 1) == -1)
return -1;
else
return cb[0];
}
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
abstract public int read(char cbuf[], int off, int len) throws IOException;
private static final int maxSkipBufferSize = 8192;
private char skipBuffer[] = null;
public long skip(long n) throws IOException {
if (n < 0L)
throw new IllegalArgumentException("skip value is negative");
int nn = (int) Math.min(n, maxSkipBufferSize);
synchronized (lock) {
if ((skipBuffer == null) || (skipBuffer.length < nn))
skipBuffer = new char[nn];
long r = n;
while (r > 0) {
int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
if (nc == -1)
break;
r -= nc;
}
return n - r;
}
}
public boolean ready() throws IOException {
return false;
}
public boolean markSupported() {
return false;
}
public void mark(int readAheadLimit) throws IOException {
throw new IOException("mark() not supported");
}
public void reset() throws IOException {
throw new IOException("reset() not supported");
}
abstract public void close() throws IOException;
}
ReaderとInputStreamは少し似ていることがわかります.例えば、Closeableというインタフェースも実現されていますし、read方法もあります.read方法には3つの実現方法があり、skip方法もあります.それらの役割も悪くありませんが、ここでのread方法は文字ストリームのデータを読み出しています.さらにskipと前のInputStreamのskipは少し違いますが、ここのskipはロックされています.このskipマルチスレッドの方法はスレッドが安全であることがわかります.あまり言わないで、もう一度過去に戻って上の
InputStreamReader,InputStreamReaderクラスは4回の構造方法をリロードしたほか,2つのread方法,readyとclose方法が残っているが,ここでのreadは親Readerのものであり,上書き,すなわち書き換えが行われているはずである.ここでInputStreamReaderはReaderとInputStreamの間にあり、バイトストリームを文字ストリームに変換するのに適した役割を果たしています.
次にポイントBufferedReaderとBufferedWriterを見てみましょう
package java.io;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class BufferedReader extends Reader {
private Reader in;
private char cb[];
private int nChars, nextChar;
private static final int INVALIDATED = -2;
private static final int UNMARKED = -1;
private int markedChar = UNMARKED;
private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
/** If the next character is a line feed, skip it */
private boolean skipLF = false;
/** The skipLF flag when the mark was set */
private boolean markedSkipLF = false;
private static int defaultCharBufferSize = 8192;
private static int defaultExpectedLineLength = 80;
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
//
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) {
if (nextChar >= nChars) {
fill();
if (nextChar >= nChars)
return -1;
}
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '
') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}
private int read1(char[] cbuf, int off, int len) throws IOException {
if (nextChar >= nChars) {
/* If the requested length is at least as large as the buffer, and
if there is no mark/reset activity, and if line feeds are not
being skipped, do not bother to copy the characters into the
local buffer. In this way buffered streams will cascade
harmlessly. */
if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
return in.read(cbuf, off, len);
}
fill();
}
if (nextChar >= nChars) return -1;
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '
') {
nextChar++;
if (nextChar >= nChars)
fill();
if (nextChar >= nChars)
return -1;
}
}
int n = Math.min(len, nChars - nextChar);
System.arraycopy(cb, nextChar, cbuf, off, n);
nextChar += n;
return n;
}
public int read(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int n = read1(cbuf, off, len);
if (n <= 0) return n;
while ((n < len) && in.ready()) {
int n1 = read1(cbuf, off + n, len - n);
if (n1 <= 0) break;
n += n1;
}
return n;
}
}
String readLine(boolean ignoreLF) throws IOException {
StringBuffer s = null;
int startChar;
synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;
bufferLoop:
for (;;) {
if (nextChar >= nChars)
fill();
if (nextChar >= nChars) { /* EOF */
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
boolean eol = false;
char c = 0;
int i;
/* Skip a leftover '
', if necessary */
if (omitLF && (cb[nextChar] == '
'))
nextChar++;
skipLF = false;
omitLF = false;
charLoop:
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '
') || (c == '\r')) {
eol = true;
break charLoop;
}
}
startChar = nextChar;
nextChar = i;
if (eol) {
String str;
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}
if (s == null)
s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}
public String readLine() throws IOException {
return readLine(false);
}
// , JDK
}
以上、分析してようやくポイントが見えてきましたが、BufferedReaderソースコードからBufferedReaderにもreadメソッドとskipメソッドがあり、readLineというメソッドが多くなり、BufferedReaderもReaderクラスを継承していることがわかります.では、readLineというメソッドを具体的に見てみましょう.
中にsynchronized(lock)という言葉があるのが見えますが、このreadLineの方法はロックされていますが、これはポイントではありません.次のbufferLoopを見てください.これはポイントです.bufferLoopの中にfor(;)これはデッドサイクルなので、データの読み取りが終わるたびにブロックされます.では、第1の問題をどのように解決するかというと、第2の問題のようにreadLineをループに配置することで、「または'r」を読み取るたびにreadlineは値を返し、whileループに入り、ループのコードも読み取り後に実行されます.ここでreadlineという方法は、リターン文字または改行文字を読み出すたびに返され、クライアントがメッセージを送信する最後にリターン文字または改行文字を付けないと、readlineは値を返すことができません.2つ目の問題は、handlerを静的変数に設定して解決したのですが、androidクライアントはreadLineがデータを読み出すたびにメインスレッド(UIスレッド)にメッセージを転送し、最初は前回のhandler値を呼び出していたので、後でhandlerを静的に設定して問題が解決したのですが、なぜそうなったのでしょうか.
これはJavaにおけるメモリ割り当てに関し,Javaにおけるメモリは一般にメソッド領域,スタック,JVMメソッドスタック,ローカルメソッドスタックに分けられる.レジスタ(cache)一般的な静的変数はメソッド領域に存在し、newからのオブジェクトはスタックメモリに存在し、handlerがグローバル変数に設定されている場合、この変数の値はnewからのオブジェクトと同様にスタックメモリに存在する.各スレッドは独自のJVMスタックメモリを有し、1つのスレッドは他のスレッドのJVMスタックの変数とメソッドにアクセスできない.ここまで言うと、変数値がスタックメモリに存在し、変数がJVMスタックに存在するのは疑問です.
JVMは新しく作成したスレッドごとにスタックを割り当て、各javaアプリケーションはJVM実列に対応し、各インスタンスはスタックに対応します.つまりjavaアプリケーションの各スレッドのそれぞれのスタックはjavaアプリケーションというスタックの中にあります.静的変数は一般的にメソッド領域に格納され、メソッド領域もグローバルに共有され、つまりすべてのスレッドが共有されています.もちろんjavaアプリケーションのスタックもグローバル共有であり、JVMスタック、ローカルメソッドスタック、レジスタは非共有である.
したがって,タイトルの問題に戻り,handlerを静的変数に設定すると,グローバル共有となり,すべてのスレッドがアクセスできるようになったので,whileサイクルでreadLineで読み取った情報をhandlerで送信するプライマリスレッド,handlerの値が前のオブジェクトの値であるかという問題を解決した.他は分析を展開しないで、更に展開して私が気を失うことを恐れて、以上はすべて個人の理解で、もし間違いがあれば指摘してください.