Java中使用BufferedReader中的readLine方法遇到的一些问题(readLine阻塞)

在做一个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循环执行一次,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方法,发现这里有两个read方法,一个是抽象方法,还有一个read则是重载了上面这个read抽象方法,对其进行了具体实现,read方法主要是把输入流中读取的数据存到一个数组中,具体的就不再一一分析了。然后skip方法相信大家应该也很少用到,作用就是跳过或者舍弃输入流数据中的n个字节。另一个类OutputStream就先不分析了,这里这要分析read。

然后再来看InputStreamReader和OutputStreamWriter这两个类

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方法有三种实现方法,也有skip方法,它们的作用也是差不多的,只不过这里的read方法读取的是字符流中的数据了。再仔细看skip和前面的InputStream中的skip有些不同了,这里的skip加锁了,可见这个skip多线程方法是线程安全的,不多说了,再回过去看下上面的

InputStreamReader,InputStreamReader类除了重载了四次构造方法,就剩下两个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] == '\n') {
                        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] == '\n') {
                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 '\n', if necessary */
                if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
                skipLF = false;
                omitLF = false;

            charLoop:
                for (i = nextChar; i < nChars; i++) {
                    c = cb[i];
                    if ((c == '\n') || (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 (;;)这是一个死循环,所以导致每次读取完数据都会阻塞,那么怎么解决第一个问题呢,就是如同在第二个问题中那样,把readLine放到循环中,这样每读取到一次'\n'或者'\r',readline就会返回一个值,接着进入while循环,循环中的代码也会在读取后进行执行,这里注意readline这个方法每次读取到回车符或者换行符才会进行返回,如果在客户端发送消息的末尾没有加回车符或者换行符,那么readline就无法返回值。第二个问题,我是把handler设置成了静态变量才解决的,android客户端每次readLine读取数据后都要把消息传会给主线程(UI线程),然后开始都是调用前一次的handler值,所以后面把handler设置成了静态,问题就解决了,为什么会这样呢?

这个涉及到Java中内存分配了,在Java中内存一般分为方法区,堆,JVM方法栈,本地方法栈,寄存器(cache),一般的静态变量都存在方法区中,而new出来的对象都是在堆内存中,如果handler设置成了全局变量,那么这个变量的值就和new出来的对象一样一起存在堆内存中。而每个线程都是有自己的JVM栈内存的,一个线程访问不到其他线程的JVM栈中的变量和方法的,说到这里又会有疑问了,怎么又是变量值存在堆内存中,又是变量在JVM栈中。

  • 栈 :局部变量(包括局部变量的值),静态变量的指针(引用),方法,对象的指针(引用),全局变量的指针(引用),数组的指针(引用) 等
  • 堆:new出来的对象(对象中包含全局变量指针指向的真实值及其他),数组中存储的值 等
  • 方法区:静态变量指针(引用)所指向的真实值,类名,方法信息,程序编译后的代码 等

其实在Java类中定义出一个全局变量,就会把这个变量的指针存放在栈中,然后这个全局变量指向的内存块则是在堆中的,也就是真是数据是存放在堆里面的(因为全局变量在对象中),栈中只是存放了这个数据的地址而已,这样才能找到这个值,如果这个指针指向了别的内存地址了,那么前面那个内存块就再也无法访问到它了,然后JVM就会利用垃圾回收机制清理掉它。

JVM会为每个新创建的线程都分配一个堆栈,每个java应用都对应一个JVM实列,每个实例对应一个堆,也就是java应用中的每个线程各自的堆都是在java应用这个大堆里面的,而静态变量一般都是存放在方法区中,方法区也是全局共享的,也就是所有线程共享的,当然java应用中的那个堆也是全局共享的,JVM栈,本地方法栈和寄存器是非共享的。

所以回到题目中的问题,把handler设置为静态变量的时候,它就是全局共享了,所有线程都能访问到它,所以就解决了while循环中把readLine读取的信息通过handler发送的主线程,handler的值还是前面那个对象中值的问题。另外的就不再展开分析了,再展开我怕我要绕晕了,以上都是个人理解,如果有错请指正。

你可能感兴趣的:(JAVA)