Java SE基礎(chǔ)鞏固(六):Java IO

到現(xiàn)在為止,Java IO可分為三類:BIO火架、NIO鉴象、AIO。最早出現(xiàn)的是BIO距潘,然后是NIO炼列,最近的是AIO,BIO即Blocking IO音比,NIO有的文章說是New NIO俭尖,也有的文章說是No Blocking IO,我查了一些資料洞翩,官網(wǎng)說的應(yīng)該是No Blocking IO稽犁,提供了Selector,Channle骚亿,SelectionKey抽象已亥,AIO即Asynchronous IO(異步IO),提供了Fauture等異步操作来屠。

1 BIO

上圖是BIO的架構(gòu)體系圖虑椎。可以看到BIO主要分為兩類IO俱笛,即字符流IO和字節(jié)流IO捆姜,字符流即把輸入輸出數(shù)據(jù)當(dāng)做字符來看待,Writer和Reader是其繼承體系的最高層迎膜,字節(jié)流即把輸入輸出當(dāng)做字節(jié)來看待泥技,InputStream和OutputStream是其繼承體系的最高層。下面以文件操作為例磕仅,其他的實現(xiàn)類也非常類似珊豹。

順便說一下簸呈,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream店茶,使其擁有了緩沖的能力蜕便。

1.1 字節(jié)流

public class Main {

    public static void main(String[] args) throws IOException {
        //寫入文件
        FileOutputStream out = new FileOutputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        out.write("hello,world".getBytes("UTF-8"));
        out.flush();
        out.close();

        //讀取文件
        FileInputStream in = new FileInputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        byte[] buffer = new byte[in.available()];
        in.read(buffer);
        System.out.println(new String(buffer, "UTF-8"));
        in.close();
    }
}

向FileOutputStream構(gòu)造函數(shù)中傳入文件名來創(chuàng)建FileOutputStream對象,即打開了一個字節(jié)流忽妒,之后使用write方法向字節(jié)流中寫入數(shù)據(jù)玩裙,完成之后調(diào)用flush刷新緩沖區(qū),最后記得要關(guān)閉字節(jié)流段直。讀取文件也是類似的吃溅,先打開一個字節(jié)流,然后從字節(jié)流中讀取數(shù)據(jù)并存入內(nèi)存中(buffer數(shù)組)鸯檬,然后再關(guān)閉字節(jié)流决侈。

因為InputStream和OutputStream都繼承了AutoCloseable接口,所以如果使用的是try-resource的語法來進(jìn)行字節(jié)流的IO操作喧务,可不需要手動顯式調(diào)用close方法了赖歌,這也是非常推薦的做法,在示例中我沒有這樣做只是為了方便功茴。

1.2 字符流

字節(jié)流主要使用的是InputStream和OutputStream庐冯,而字符流主要使用的就是與之對應(yīng)的Reader和Writer。下面來看一個示例坎穿,該示例的功能和上述示例的一樣展父,只不過實現(xiàn)手段不同:

public class Main {

    public static void main(String[] args) throws IOException {
        Writer writer = new FileWriter("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        writer.write("hello,world\n");
        writer.write("hello,yeonon\n");
        writer.flush();
        writer.close();

        BufferedReader reader = new BufferedReader(new FileReader("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt"));

        String line = "";
        int lineCount = 0;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
            lineCount++;
        }
        reader.close();
        System.out.println(lineCount);
    }
}

Writer非常簡單,無法就是打開字符流玲昧,然后向字符流中寫入字符栖茉,然后關(guān)閉。關(guān)鍵是Reader孵延,示例代碼中使用了BufferedReader來包裝FileReader吕漂,使得原本沒有緩沖功能的FileReader有了緩沖功能,這就是上面提到過的裝飾者模式尘应,BufferedReader還提供了方便使用的API惶凝,例如readLine(),這個方法每次調(diào)用會讀取文件中的一行犬钢。

以上就是BIO的簡單使用梨睁,源碼的話因為涉及太多的底層,所以如果對底層不是很了解的話會很難理解源碼娜饵。

2 NIO

BIO是同步阻塞的IO,而NIO是同步非阻塞的IO官辈。NIO中有幾個比較重要的組件:Selector箱舞,SelectionKey遍坟,Channel,ByteBuffer晴股,其中Selector就是所謂的選擇器愿伴,SelectionKey可以簡單理解為選擇鍵,這個鍵將Selector和Channle進(jìn)行一個綁定(或者所Channle注冊到Selector上)电湘,當(dāng)有數(shù)據(jù)到達(dá)Channel的時候隔节,Selector會從阻塞狀態(tài)中恢復(fù)過來,并對該Channle進(jìn)行操作寂呛,并且怎诫,我們不能直接對Channle進(jìn)行讀寫操作,只能對ByteBuffer操作贷痪。如下圖所示:

下面是一個Socket網(wǎng)絡(luò)編程的例子:

//服務(wù)端
public class SocketServer {

    private Selector selector;
    private final static int port = 9000;
    private final static int BUF = 10240;

    private void init() throws IOException {
        //獲取一個Selector
        selector = Selector.open();
        //獲取一個服務(wù)端socket Channel
        ServerSocketChannel channel = ServerSocketChannel.open();
        //設(shè)置為非阻塞模式
        channel.configureBlocking(false);
        //綁定端口
        channel.socket().bind(new InetSocketAddress(port));
        //把channle注冊到Selector上幻妓,并表示對ACCEPT事件感興趣
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            //該方法會阻塞,直到和其綁定的任何一個channel有數(shù)據(jù)過來
            selector.select();
            //獲取該Selector綁定的SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //記得刪除劫拢,否則就無限循環(huán)了
                iterator.remove();
                //如果該事件是一個ACCEPT肉津,那么就執(zhí)行doAccept方法,其他的也一樣
                if (key.isAcceptable()) {
                    doAccept(key);
                } else if (key.isReadable()) {
                    doRead(key);
                } else if (key.isWritable()) {
                    doWrite(key);
                } else if (key.isConnectable()) {
                    System.out.println("連接成功舱沧!");
                }
            }
        }
    }

    //寫方法妹沙,注意不能直接對channle進(jìn)行讀寫操作,只能對ByteBuffer進(jìn)行操作
    private void doWrite(SelectionKey key) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.flip();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
        buffer.compact();
    }

    //讀取消息
    private void doRead(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        long reads = socketChannel.read(buffer);
        while (reads > 0) {
            buffer.flip();
            byte[] data = buffer.array();
            System.out.println("讀取到消息: " + new String(data, "UTF-8"));
            buffer.clear();
            reads = socketChannel.read(buffer);
        }
        if (reads == -1) {
            socketChannel.close();
        }
    }

    //當(dāng)有連接過來的時候熟吏,獲取連接過來的channle距糖,然后注冊到Selector上,并設(shè)置成對讀消息感興趣分俯,當(dāng)客戶端有消息過來的時候肾筐,Selector就可以讓其執(zhí)行doRead方法,然后讀取消息并打印缸剪。
    private void doAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        System.out.println("服務(wù)端監(jiān)聽中...");
        SocketChannel socketChannel = serverChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ);
    }
    
    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.init();
    }
}

//客戶端吗铐,寫得比較簡單
public class SocketClient {

    private final static int port = 9000;
    private final static int BUF = 10240;


    private void init() throws IOException {
        //獲取channel
        SocketChannel channel = SocketChannel.open();
        //連接到遠(yuǎn)程服務(wù)器
        channel.connect(new InetSocketAddress(port));
        //設(shè)置非阻塞模式
        channel.configureBlocking(false);
        //往ByteBuffer里寫消息
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.put("Hello,Server".getBytes("UTF-8"));
        buffer.flip();
        //將ByteBuffer內(nèi)容寫入Channle,即發(fā)送消息
        channel.write(buffer);
        channel.close();
    }


    public static void main(String[] args) throws IOException {
        SocketClient client = new SocketClient();
            client.init();
    }
}

嘗試啟動一個服務(wù)端杏节,多個客戶端唬渗,結(jié)果大致如下所示:

服務(wù)端監(jiān)聽中...
讀取到消息: Hello,Server                       
服務(wù)端監(jiān)聽中...
讀取到消息: Hello,Server  

注釋寫得挺清楚了,我這里只是簡單使用了NIO奋渔,但實際上NIO遠(yuǎn)遠(yuǎn)不止這些東西镊逝,光一個ByteBuffer就能說一天,如果有機(jī)會嫉鲸,我會在后面Netty相關(guān)的文章中詳細(xì)說一下這幾個組件撑蒜。在此就不再多說了。

吐槽一些,純NIO寫的服務(wù)端和客戶端實在是太麻煩了座菠,一不小心就會寫錯狸眼,還是使用Netty類似的框架好一些啊。

3 AIO

在JDK7中新增了一些IO相關(guān)的API浴滴,這些API稱作AIO拓萌。因為其提供了一些異步操作IO的功能,但本質(zhì)是其實還是NIO升略,所以可以簡單的理解為是NIO的擴(kuò)充微王。AIO中最重要的就是Future了,F(xiàn)uture表示將來的意思品嚣,即這個操作可能會持續(xù)很長時間炕倘,但我不會等,而是到將來操作完成的時候腰根,再過來通知我激才,這就是異步的意思。下面是兩個使用AIO的例子:

    public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
        Path path = Paths.get("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Future<Integer> future = channel.read(buffer,0);
        Integer readNum = future.get(); //阻塞额嘿,如果不調(diào)用該方法瘸恼,main方法會繼續(xù)執(zhí)行
        buffer.flip();
        System.out.println(new String(buffer.array(), "UTF-8"));
        System.out.println(readNum);
    }

第一個例子使用AsynchronousFileChannel來異步的讀取文件內(nèi)容,在代碼中册养,我使用了future.get()方法东帅,該方法會阻塞當(dāng)前線程,在例子中即主線程球拦,當(dāng)工作線程靠闭,即讀取文件的線程執(zhí)行完畢后才會從阻塞狀態(tài)中恢復(fù)過來,并將結(jié)果返回坎炼。之后就可以從ByteBuffer中讀取數(shù)據(jù)了愧膀。這是使用將來時的例子,下面來看看使用回調(diào)的例子:

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
        Path path = Paths.get("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("完成讀取");
                try {
                    System.out.println(new String(attachment.array(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println("讀取失敗");
            }
        });
        System.out.println("繼續(xù)執(zhí)行主線程");
        //調(diào)用完成之后不需要等待任務(wù)完成谣光,會直接繼續(xù)執(zhí)行主線程
        while (true) {
            Thread.sleep(1000);
        }
    }
}

輸出的結(jié)果大致如下所示檩淋,但不一定,這取決于線程調(diào)度:

繼續(xù)執(zhí)行主線程
完成讀取

hello,world
hello,yeonon

當(dāng)任務(wù)完成萄金,即讀取文件完畢的時候蟀悦,會調(diào)用completed方法,失敗會調(diào)用failed方法氧敢,這就是回調(diào)日戈。詳細(xì)接觸過回調(diào)的朋友應(yīng)該不難理解。

4 BIO孙乖、NIO浙炼、AIO的區(qū)別

  1. BIO是同步阻塞的IO份氧,NIO是同步非阻塞IO,AIO異步非阻塞IO鼓拧,這是最基本的區(qū)別半火。阻塞模式會導(dǎo)致其他線程被IO線程阻塞,必須等待IO線程執(zhí)行完畢才能繼續(xù)執(zhí)行邏輯季俩,非阻塞和異步并不等同,非阻塞模式下梅掠,一般會采用事件輪詢的方式來執(zhí)行IO酌住,即IO多路復(fù)用,雖然仍然是同步的阎抒,但執(zhí)行效率比傳統(tǒng)的BIO要高很多酪我,AIO則是異步IO,如果把IO工作當(dāng)做一個任務(wù)的話且叁,在當(dāng)前線程中提交一個任務(wù)之后都哭,不會有阻塞,會繼續(xù)執(zhí)行當(dāng)前線程的后續(xù)邏輯逞带,在任務(wù)完成之后欺矫,當(dāng)前線程會收到通知,然后再決定如何處理展氓,這種方式的IO穆趴,CPU效率是最高的,CPU幾乎沒有發(fā)生過停頓遇汞,而時一直至于忙狀態(tài)未妹,所以效率非常高,但編程難度會比較大空入。
  2. BIO面向的是流络它,無論是字符流還是字節(jié)流,通俗的講歪赢,BIO在讀寫數(shù)據(jù)的時候會按照一個接一個的方式讀寫化戳,而NIO和AIO(因為AIO實際上是NIO的擴(kuò)充,所以從這個方面來看轨淌,可以把他們放在一塊)讀寫數(shù)據(jù)的時候是按照一塊一塊的讀取的迂烁,讀取到的數(shù)據(jù)會緩存在內(nèi)存中,然后在內(nèi)存中對數(shù)據(jù)進(jìn)行處理递鹉,這種方式的好處是減少了硬盤或者網(wǎng)絡(luò)的讀寫次數(shù)盟步,從而降低了由于硬盤或網(wǎng)絡(luò)速度慢帶來的效率影響。
  3. BIO的API雖然比較底層躏结,但如果熟悉之后編寫起來會比較容易却盘,NIO或者AIO的API抽象層次高,一般來說應(yīng)該更容易使用才是,但實際上卻很難“正確”的編寫黄橘,而且DEBUG的難度也較大兆览,這也是為什么Netty等NIO框架受歡迎的原因之一。

以上就是我理解的BIO塞关、NIO和AIO區(qū)別抬探。

5 小結(jié)

本文簡單粗略的講了一下BIO、NIO帆赢、AIO的使用小压,并未涉及源碼,也沒有涉及太多的原理椰于,如果讀者希望了解更多關(guān)于三者的內(nèi)容怠益,建議參看一些書籍,例如老外寫的《Java NIO》瘾婿,該書全面系統(tǒng)的講解了NIO的各種組件和細(xì)節(jié)蜻牢,非常推薦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末偏陪,一起剝皮案震驚了整個濱河市抢呆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌竹挡,老刑警劉巖镀娶,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異揪罕,居然都是意外死亡梯码,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門好啰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轩娶,“玉大人,你說我怎么就攤上這事框往■悖” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵椰弊,是天一觀的道長许溅。 經(jīng)常有香客問我,道長秉版,這世上最難降的妖魔是什么贤重? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮清焕,結(jié)果婚禮上并蝗,老公的妹妹穿的比我還像新娘祭犯。我一直安慰自己,他們只是感情好滚停,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布沃粗。 她就那樣靜靜地躺著,像睡著了一般键畴。 火紅的嫁衣襯著肌膚如雪最盅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天镰吵,我揣著相機(jī)與錄音檩禾,去河邊找鬼。 笑死疤祭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饵婆。 我是一名探鬼主播勺馆,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侨核!你這毒婦竟也來了草穆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤搓译,失蹤者是張志新(化名)和其女友劉穎悲柱,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體些己,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡豌鸡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了段标。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涯冠。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逼庞,靈堂內(nèi)的尸體忽然破棺而出蛇更,到底是詐尸還是另有隱情,我是刑警寧澤赛糟,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布派任,位于F島的核電站,受9級特大地震影響璧南,放射性物質(zhì)發(fā)生泄漏掌逛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一穆咐、第九天 我趴在偏房一處隱蔽的房頂上張望颤诀。 院中可真熱鬧字旭,春花似錦、人聲如沸崖叫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽心傀。三九已至屈暗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間脂男,已是汗流浹背养叛。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留宰翅,地道東北人弃甥。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像汁讼,于是被迫代替她去往敵國和親淆攻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容