JavaでBufferedReaderのreadLineメソッドを使用して発生した問題(readLineブロック)

16924 ワード

Socket通信のアイテムを作成する際にBufferedReaderを使用し、readLine関数で情報を読み取りますが、readLineを読み取りメッセージのループの外に置いた後、プログラムが次の文を実行できなくなり、GitHub接続に対応します(後でGitHubブログで更新し、GitHubブログを基準にします).
    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スタックに存在するのは疑問です.
  • スタック:ローカル変数(ローカル変数の値を含む)、静的変数のポインタ(参照)、メソッド、オブジェクトのポインタ(参照)、グローバル変数のポインタ(参照)、配列のポインタ(参照)など
  • スタック:newから出るオブジェクト(オブジェクトにはグローバル変数ポインタが指す真実値およびその他が含まれる)、配列に格納された値など
  • .
  • メソッド領域:静的変数ポインタ(参照)が指す真実値、クラス名、メソッド情報、プログラムコンパイル後のコードなど
  • Javaクラスでグローバル変数を定義すると、この変数のポインタがスタックに格納され、このグローバル変数が指すメモリブロックがスタックに格納されます.つまり、本当にデータはスタックに格納されています(グローバル変数がオブジェクトに格納されているため)、スタックにはこのデータのアドレスが格納されているだけです.そうすれば、このポインタが他のメモリアドレスを指している場合、前のメモリブロックは二度とアクセスできません.その後、JVMはゴミ回収メカニズムを利用してクリーンアップされます.
    JVMは新しく作成したスレッドごとにスタックを割り当て、各javaアプリケーションはJVM実列に対応し、各インスタンスはスタックに対応します.つまりjavaアプリケーションの各スレッドのそれぞれのスタックはjavaアプリケーションというスタックの中にあります.静的変数は一般的にメソッド領域に格納され、メソッド領域もグローバルに共有され、つまりすべてのスレッドが共有されています.もちろんjavaアプリケーションのスタックもグローバル共有であり、JVMスタック、ローカルメソッドスタック、レジスタは非共有である.
    したがって,タイトルの問題に戻り,handlerを静的変数に設定すると,グローバル共有となり,すべてのスレッドがアクセスできるようになったので,whileサイクルでreadLineで読み取った情報をhandlerで送信するプライマリスレッド,handlerの値が前のオブジェクトの値であるかという問題を解決した.他は分析を展開しないで、更に展開して私が気を失うことを恐れて、以上はすべて個人の理解で、もし間違いがあれば指摘してください.