- 從 Buffer 讀取數據到 Channel;
- 使用 get() 方法從 Buffer 中讀取數據。
</ol>
Buffer 的 rewin 方法將 position 設回 0,所以你可以重讀 Buffer 中的所有數據。limit 保持不變,仍然表示能從 Buffer 中讀取多少個元素(byte、char 等)。
clear() 和 compact() 方法
一旦讀完 Buffer 中的數據,需要讓 Buffer 準備好再次被寫入。可以通過 clear() 或 compact() 方法來完成。
如果調用的是 clear() 方法,position 將被設回 0,limit 被設置成 capacity 的值。換句話說,Buffer 被清空了。Buffer 中的數據并未清除,只是這些標記告訴我們可以從哪里開始往 Buffer 里寫數據。
如果 Buffer 中有一些未讀的數據,調用 clear() 方法,數據將“被遺忘”,意味著不再有任何標記會告訴你哪些數據被讀過,哪些還沒有。如果 Buffer 中仍有未讀的數據,且后續還需要這些數據,但是此時想要先寫些數據,那么使用 compact() 方法。compact() 方法將所有未讀的數據拷貝到 Buffer 起始處。然后將 position 設到最后一個未讀元素正后面。limit 屬性依然像 clear() 方法一樣,設置成 capacity。現在 Buffer 準備好寫數據了,但是不會覆蓋未讀的數據。
Buffer 參數
Buffer 有 3 個重要的參數:位置 (position)、容量 (capacity) 和上限 (limit)。
capacity 是指 Buffer 的大小,在 Buffer 建立的時候已經確定。
limit 當 Buffer 處于寫模式,指還可以寫入多少數據;處于讀模式,指還有多少數據可以讀。
position 當 Buffer 處于寫模式,指下一個寫數據的位置;處于讀模式,當前將要讀取的數據的位置。每讀寫一個數據,position+1,也就是 limit 和 position 在 Buffer 的讀/寫時的含義不一樣。當調用 Buffer 的 flip 方法,由寫模式變為讀模式時,limit(讀)=position(寫),position(讀) =0。
散射&聚集
NIO 提供了處理結構化數據的方法,稱之為散射 (Scattering) 和聚集 (Gathering)。散射是指將數據讀入一組 Buffer 中,而不僅僅是一個。聚集與之相反,指將數據寫入一組 Buffer 中。散射和聚集的基本使用方法和對單個 Buffer 操作時的使用方法相當類似。在散射讀取中,通道依次填充每個緩沖區。填滿一個緩沖區后,它就開始填充下一個,在某種意義上,緩沖區數組就像一個大緩沖區。在已知文件具體結構的情況下,可以構造若干個符合文件結構的 Buffer,使得各個 Buffer 的大小恰好符合文件各段結構的大小。此時,通過散射讀的方式可以一次將內容裝配到各個對應的 Buffer 中,從而簡化操作。如果需要創建指定格式的文件,只要先構造好大小合適的 Buffer 對象,使用聚集寫的方式,便可以很快地創建出文件。清單 1 以 FileChannel 為例,展示如何使用散射和聚集讀寫結構化文件。
清單 1. 使用散射和聚集讀寫結構化文件
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOScatteringandGathering {
public void createFiles(String TPATH){
try {
ByteBuffer bookBuf = ByteBuffer.wrap("java 性能優化技巧".getBytes("utf-8"));
ByteBuffer autBuf = ByteBuffer.wrap("test".getBytes("utf-8"));
int booklen = bookBuf.limit();
int autlen = autBuf.limit();
ByteBuffer[] bufs = new ByteBuffer[]{bookBuf,autBuf};
File file = new File(TPATH);
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
FileOutputStream fos = new FileOutputStream(file);
FileChannel fc = fos.getChannel();
fc.write(bufs);
fos.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ByteBuffer b1 = ByteBuffer.allocate(booklen);
ByteBuffer b2 = ByteBuffer.allocate(autlen);
ByteBuffer[] bufs1 = new ByteBuffer[]{b1,b2};
File file1 = new File(TPATH);
try {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
fc.read(bufs1);
String bookname = new String(bufs1[0].array(),"utf-8");
String autname = new String(bufs1[1].array(),"utf-8");
System.out.println(bookname+" "+autname);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args){
NIOScatteringandGathering nio = new NIOScatteringandGathering();
nio.createFiles("C:\1.TXT");
}
}</pre>
輸出如下清單 2 所示。
清單 2. 運行結果
清單 3 所示代碼對傳統 I/O、基于 Byte 的 NIO、基于內存映射的 NIO 三種方式進行了性能上的對比,使用一個有 400 萬數據的文件的讀、寫操作耗時作為評測依據。
清單 3. I/O 的三種方式對比試驗
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class NIOComparator {
public void IOMethod(String TPATH){
long start = System.currentTimeMillis();
try {
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(new File(TPATH))));
for(int i=0;i<4000000;i++){
dos.writeInt(i);//寫入 4000000 個整數
}
if(dos!=null){
dos.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
try {
DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(new File(TPATH))));
for(int i=0;i<4000000;i++){
dis.readInt();
}
if(dis!=null){
dis.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(end - start);
}
public void ByteMethod(String TPATH){
long start = System.currentTimeMillis();
try {
FileOutputStream fout = new FileOutputStream(new File(TPATH));
FileChannel fc = fout.getChannel();//得到文件通道
ByteBuffer byteBuffer = ByteBuffer.allocate(4000000*4);//分配 Buffer
for(int i=0;i<4000000;i++){
byteBuffer.put(int2byte(i));//將整數轉為數組
}
byteBuffer.flip();//準備寫
fc.write(byteBuffer);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
FileInputStream fin;
try {
fin = new FileInputStream(new File(TPATH));
FileChannel fc = fin.getChannel();//取得文件通道
ByteBuffer byteBuffer = ByteBuffer.allocate(4000000*4);//分配 Buffer
fc.read(byteBuffer);//讀取文件數據
fc.close();
byteBuffer.flip();//準備讀取數據
while(byteBuffer.hasRemaining()){
byte2int(byteBuffer.get(),byteBuffer.get(),byteBuffer.get(),byteBuffer.get());//將 byte 轉為整數
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(end - start);
}
public void mapMethod(String TPATH){
long start = System.currentTimeMillis();
//將文件直接映射到內存的方法
try {
FileChannel fc = new RandomAccessFile(TPATH,"rw").getChannel();
IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, 4000000*4).asIntBuffer();
for(int i=0;i<4000000;i++){
ib.put(i);
}
if(fc!=null){
fc.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
try {
FileChannel fc = new FileInputStream(TPATH).getChannel();
MappedByteBuffer lib = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
lib.asIntBuffer();
while(lib.hasRemaining()){
lib.get();
}
if(fc!=null){
fc.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(end - start);
}
public static byte[] int2byte(int res){
byte[] targets = new byte[4];
targets[3] = (byte)(res & 0xff);//最低位
targets[2] = (byte)((res>>8)&0xff);//次低位
targets[1] = (byte)((res>>16)&0xff);//次高位
targets[0] = (byte)((res>>>24));//最高位,無符號右移
return targets;
}
public static int byte2int(byte b1,byte b2,byte b3,byte b4){
return ((b1 & 0xff)<<24)|((b2 & 0xff)<<16)|((b3 & 0xff)<<8)|(b4 & 0xff);
}
public static void main(String[] args){
NIOComparator nio = new NIOComparator();
nio.IOMethod("c:\\1.txt");
nio.ByteMethod("c:\\2.txt");
nio.ByteMethod("c:\\3.txt");
}
}
清單 3 運行輸出如清單 4 所示。
清單 4. 運行輸出
除上述描述及清單 3 所示代碼以外,NIO 的 Buffer 還提供了一個可以直接訪問系統物理內存的類 DirectBuffer。DirectBuffer 繼承自 ByteBuffer,但和普通的 ByteBuffer 不同。普通的 ByteBuffer 仍然在 JVM 堆上分配空間,其最大內存受到最大堆的限制,而 DirectBuffer 直接分配在物理內存上,并不占用堆空間。在對普通的 ByteBuffer 訪問時,系統總是會使用一個“內核緩沖區”進行間接的操作。而 DirectrBuffer 所處的位置,相當于這個“內核緩沖區”。因此,使用 DirectBuffer 是一種更加接近系統底層的方法,所以,它的速度比普通的 ByteBuffer 更快。DirectBuffer 相對于 ByteBuffer 而言,讀寫訪問速度快很多,但是創建和銷毀 DirectrBuffer 的花費卻比 ByteBuffer 高。DirectBuffer 與 ByteBuffer 相比較的代碼如清單 5 所示。
清單 5. DirectBuffer VS ByteBuffer
import java.nio.ByteBuffer;
public class DirectBuffervsByteBuffer {
public void DirectBufferPerform(){
long start = System.currentTimeMillis();
ByteBuffer bb = ByteBuffer.allocateDirect(500);//分配 DirectBuffer
for(int i=0;i<100000;i++){
for(int j=0;j<99;j++){
bb.putInt(j);
}
bb.flip();
for(int j=0;j<99;j++){
bb.getInt(j);
}
}
bb.clear();
long end = System.currentTimeMillis();
System.out.println(end-start);
start = System.currentTimeMillis();
for(int i=0;i<20000;i++){
ByteBuffer b = ByteBuffer.allocateDirect(10000);//創建 DirectBuffer
}
end = System.currentTimeMillis();
System.out.println(end-start);
}
public void ByteBufferPerform(){
long start = System.currentTimeMillis();
ByteBuffer bb = ByteBuffer.allocate(500);//分配 DirectBuffer
for(int i=0;i<100000;i++){
for(int j=0;j<99;j++){
bb.putInt(j);
}
bb.flip();
for(int j=0;j<99;j++){
bb.getInt(j);
}
}
bb.clear();
long end = System.currentTimeMillis();
System.out.println(end-start);
start = System.currentTimeMillis();
for(int i=0;i<20000;i++){
ByteBuffer b = ByteBuffer.allocate(10000);//創建 ByteBuffer
}
end = System.currentTimeMillis();
System.out.println(end-start);
}
public static void main(String[] args){
DirectBuffervsByteBuffer db = new DirectBuffervsByteBuffer();
db.ByteBufferPerform();
db.DirectBufferPerform();
}
}
運行輸出如清單 6 所示。
清單 6. 運行輸出
由清單 6 可知,頻繁創建和銷毀 DirectBuffer 的代價遠遠大于在堆上分配內存空間。使用參數-XX:MaxDirectMemorySize=200M –Xmx200M 在 VM Arguments 里面配置最大 DirectBuffer 和最大堆空間,代碼中分別請求了 200M 的空間,如果設置的堆空間過小,例如設置 1M,會拋出錯誤如清單 7 所示。
清單 7. 運行錯誤
Error occurred during initialization of VM
Too small initial heap for new size specified
DirectBuffer 的信息不會打印在 GC 里面,因為 GC 只記錄了堆空間的內存回收。可以看到,由于 ByteBuffer 在堆上分配空間,因此其 GC 數組相對非常頻繁,在需要頻繁創建 Buffer 的場合,由于創建和銷毀 DirectBuffer 的代碼比較高昂,不宜使用 DirectBuffer。但是如果能將 DirectBuffer 進行復用,可以大幅改善系統性能。清單 8 是一段對 DirectBuffer 進行監控代碼。
清單 8. 對 DirectBuffer 監控代碼
import java.lang.reflect.Field;
public class monDirectBuffer {
public static void main(String[] args){
try {
Class c = Class.forName("java.nio.Bits");//通過反射取得私有數據
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
Field reservedMemory = c.getDeclaredField("reservedMemory");
reservedMemory.setAccessible(true);
synchronized(c){
Long maxMemoryValue = (Long)maxMemory.get(null);
Long reservedMemoryValue = (Long)reservedMemory.get(null);
System.out.println("maxMemoryValue="+maxMemoryValue);
System.out.println("reservedMemoryValue="+reservedMemoryValue);
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
運行輸出如清單 9 所示。
清單 9. 運行輸出
maxMemoryValue=67108864
reservedMemoryValue=0
由于 NIO 使用起來較為困難,所以許多公司推出了自己封裝 JDK NIO 的框架,例如 Apache 的 Mina,JBoss 的 Netty,Sun 的 Grizzly 等等,這些框架都直接封裝了傳輸層的 TCP 或 UDP 協議,其中 Netty 只是一個 NIO 框架,它不需要 Web 容器的額外支持,也就是說不限定 Web 容器。
Java AIO
AIO 相關的類和接口:
- java.nio.channels.AsynchronousChannel:標記一個 Channel 支持異步 IO 操作;
- java.nio.channels.AsynchronousServerSocketChannel:ServerSocket 的 AIO 版本,創建 TCP 服務端,綁定地址,監聽端口等;
- java.nio.channels.AsynchronousSocketChannel:面向流的異步 Socket Channel,表示一個連接;
- java.nio.channels.AsynchronousChannelGroup:異步 Channel 的分組管理,目的是為了資源共享。一個 AsynchronousChannelGroup 綁定一個線程池,這個線程池執行兩個任務:處理 IO 事件和派發 CompletionHandler。AsynchronousServerSocketChannel 創建的時候可以傳入一個 AsynchronousChannelGroup,那么通過 AsynchronousServerSocketChannel 創建的 AsynchronousSocketChannel 將同屬于一個組,共享資源;
- java.nio.channels.CompletionHandler:異步 IO 操作結果的回調接口,用于定義在 IO 操作完成后所作的回調工作。AIO 的 API 允許兩種方式來處理異步操作的結果:返回的 Future 模式或者注冊 CompletionHandler,推薦用 CompletionHandler 的方式,這些 handler 的調用是由 AsynchronousChannelGroup 的線程池派發的。這里線程池的大小是性能的關鍵因素。
這里舉一個程序范例,簡單介紹一下 AIO 如何運作。
清單 10. 服務端程序
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
public class SimpleServer {
public SimpleServer(int port) throws IOException {
final AsynchronousServerSocketChannel listener =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(port));
//監聽消息,收到后啟動 Handle 處理模塊
listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
public void completed(AsynchronousSocketChannel ch, Void att) {
listener.accept(null, this);// 接受下一個連接
handle(ch);// 處理當前連接
}
@Override
public void failed(Throwable exc, Void attachment) {
// TODO Auto-generated method stub
}
});
}
public void handle(AsynchronousSocketChannel ch) {
ByteBuffer byteBuffer = ByteBuffer.allocate(32);//開一個 Buffer
try {
ch.read(byteBuffer).get();//讀取輸入
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
byteBuffer.flip();
System.out.println(byteBuffer.get());
// Do something
}
}
清單 11. 客戶端程序
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class SimpleClientClass {
private AsynchronousSocketChannel client;
public SimpleClientClass(String host, int port) throws IOException,
InterruptedException, ExecutionException {
this.client = AsynchronousSocketChannel.open();
Future<?> future = client.connect(new InetSocketAddress(host, port));
future.get();
}
public void write(byte b) {
ByteBuffer byteBuffer = ByteBuffer.allocate(32);
System.out.println("byteBuffer="+byteBuffer);
byteBuffer.put(b);//向 buffer 寫入讀取到的字符
byteBuffer.flip();
System.out.println("byteBuffer="+byteBuffer);
client.write(byteBuffer);
}
}
清單 12.Main 函數
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
public class AIODemoTest {
@Test
public void testServer() throws IOException, InterruptedException {
SimpleServer server = new SimpleServer(9021);
Thread.sleep(10000);//由于是異步操作,所以睡眠一定時間,以免程序很快結束
}
@Test
public void testClient() throws IOException, InterruptedException, ExecutionException {
SimpleClientClass client = new SimpleClientClass("localhost", 9021);
client.write((byte) 11);
}
public static void main(String[] args){
AIODemoTest demoTest = new AIODemoTest();
try {
demoTest.testServer();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
demoTest.testClient();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
后續會專門出文章具體深入介紹 AIO 的源代碼、設計理念、設計模式等等。
結束語
I/O 與 NIO 一個比較重要的區別是我們使用 I/O 的時候往往會引入多線程,每個連接使用一個單獨的線程,而 NIO 則是使用單線程或者只使用少量的多線程,每個連接共用一個線程。而由于 NIO 的非阻塞需要一直輪詢,比較消耗系統資源,所以異步非阻塞模式 AIO 就誕生了。本文對 I/O、NIO、AIO 等三種輸入輸出操作方式進行一一介紹,力求通過簡單的描述和實例讓讀者能夠掌握基本的操作、優化方法。
本文由用戶
jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
sesese色