轉載請註明原創出處,謝謝!
上篇NIO相關基礎篇一,主要介紹了一些基本的概念以及緩衝區(Buffer)和通道(Channel),本篇繼續NIO相關話題內容,主要就是檔案鎖、以及比較關鍵的Selector,後續還會繼續有一到二篇左右與NIO內容相關。
檔案鎖(FileLock)
在看RocketMQ原始碼中,發現有關於檔案鎖的import,但是具體使用程式碼裡面註釋調了[回頭看看為什麼,理解下,到時候會在某篇文章裡進行說明](實現一個事情的方法很多,所以不一定就一種),但是為了知識的完整性,還是準備講下檔案鎖,可能以後或者那個地方可以使用,或者大家在那裡使用到都可以繼續留言討論。
檔案鎖和其他我們瞭解併發裡面的鎖很多概念類似,當多個人同時操作一個檔案的時候,只有第一個人可以進行編輯,其他要麼關閉(等第一個人操作完成之後可以操作),要麼以只讀的方式進行開啟。
在java nio中提供了新的鎖檔案功能,當一個執行緒將檔案鎖定之後,其他執行緒無法操作此檔案,檔案的鎖操作是使用FileLock類來進行完成的,此類物件需要依賴FileChannel進行例項化。
檔案鎖方式:
- 共享鎖:允許多個執行緒進行檔案讀取。
- 獨佔鎖:只允許一個執行緒進行檔案的讀寫操作。
備註:檔案鎖定以整個 Java 虛擬機器來保持。但它們不適用於控制同一虛擬機器內多個執行緒對檔案的訪問。 多個併發執行緒可安全地使用檔案鎖定物件。
Java檔案依賴FileChannel的主要涉及如下4個方法:
方法 | 說明 |
---|---|
lock() | 獲取對此通道的檔案的獨佔鎖定。 |
lock(long position, long size, boolean shared) | 獲取此通道的檔案給定區域上的鎖定。 |
tryLock() throws IOException | 試圖獲取對此通道的檔案的獨佔鎖定。 |
tryLock(long position, long size, boolean shared) throws IOException | 試圖獲取對此通道的檔案給定區域的鎖定。 |
無 | lock()等同於lock(0L, Long.MAX_VALUE, false) |
無 | tryLock()等同於tryLock(0L, Long.MAX_VALUE, false) |
lock()和tryLock()的區別:
- lock()阻塞的方法,鎖定範圍可以隨著檔案的增大而增加。無參lock()預設為獨佔鎖;有參lock(0L, Long.MAX_VALUE, true)為共享鎖。
- tryLock()非阻塞,當未獲得鎖時,返回null。無參tryLock()預設為獨佔鎖;有參tryLock(0L, Long.MAX_VALUE, true)為共享鎖。
簡單例項程式碼:
File file = new File("d:" + File.separator + "test.txt") ;
FileOutputStream output = null ;
FileChannel fout = null ;
try {
output = new FileOutputStream(file,true) ;
fout = output.getChannel() ;// 得到通道
FileLock lock = fout.tryLock() ; // 進行獨佔鎖的操作
if(lock!=null){
System.out.println(file.getName() + "檔案鎖定") ;
Thread.sleep(5000) ;
lock.release() ; // 釋放
System.out.println(file.getName() + "檔案解除鎖定。") ;
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(fout!=null){
try {
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(output!=null){
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製程式碼
執行結果:
**說明:**目前也很少接觸到關於檔案鎖的靈活運用,RocketMQ那塊程式碼給註釋了,如果那我瞭解,歡迎留言討論。
Selector
說明:
- FileChannel是可讀可寫的Channel,它必須阻塞,不能用在非阻塞模式中。
- SocketChannel與FileChannel不同:新的Socket Channel能在非阻塞模式下執行並且是可選擇的。不再需要為每個socket連線指派執行緒了。使用新的NIO類,一個或多個執行緒能管理成百上千個活動的socket連線,使用Selector物件可以選擇可用的Socket Channel。
以前的Socket程式是阻塞的,伺服器必須始終等待客戶端的連線,而NIO可以通過Selector完成非阻塞操作。
備註:其實NIO主要的功能是解決服務端的通訊效能。 上篇NIO相關基礎篇一的知識,馬上這塊也是需要使用到的。
Selector一些主要方法:
方法 | 說明 |
---|---|
open() | 開啟一個選擇器。 |
select() | 選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。 |
selectedKeys() | 返回此選擇器的已選擇鍵集。 |
SelectionKey的四個重要常量:
欄位 | 說明 |
---|---|
OP_ACCEPT | 用於套接字接受操作的操作集位。 |
OP_CONNECT | 用於套接字連線操作的操作集位。 |
OP_READ | 用於讀取操作的操作集位。 |
OP_WRITE | 用於寫入操作的操作集位。 |
**說明:**其實四個常量就是Selector監聽SocketChannel四種不同型別的事件。
如果你對不止一種事件感興趣,那麼可以用"位或"操作符將常量連線起來,如下: int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
#NIO 簡單例項程式碼 服務端程式碼:
int port = 8000;
// 通過open()方法找到Selector
Selector selector = Selector.open();
// 開啟伺服器的通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 伺服器配置為非阻塞
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
// 進行服務的繫結
serverSocket.bind(address);
// 註冊到selector,等待連線
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("伺服器執行,埠:" + 8000);
// 資料緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
if ((selector.select()) > 0) { // 選擇一組鍵,並且相應的通道已經準備就緒
Set<SelectionKey> selectedKeys = selector.selectedKeys();// 取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next(); // 取出每一個key
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 接收新連線 和BIO寫法類是都是accept
SocketChannel client = server.accept();
// 配置為非阻塞
client.configureBlocking(false);
byteBuffer.clear();
// 向緩衝區中設定內容
byteBuffer.put(("當前的時間為:" + new Date()).getBytes());
byteBuffer.flip();
// 輸出內容
client.write(byteBuffer);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable() && key.isValid()) {
SocketChannel client = (SocketChannel) key.channel();
byteBuffer.clear();
// 讀取內容到緩衝區中
int readSize = client.read(byteBuffer);
if (readSize > 0) {
System.out.println("伺服器端接受客戶端資料:" + new String(byteBuffer.array(), 0, readSize));
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (key.isWritable() && key.isValid()) {
SocketChannel client = (SocketChannel) key.channel();
byteBuffer.clear();
// 向緩衝區中設定內容
byteBuffer.put(("歡迎關注匠心零度,已經收到您的反饋,會第一時間回覆您。感謝支援!!!").getBytes());
byteBuffer.flip();
// 輸出內容
client.write(byteBuffer);
client.register(selector, SelectionKey.OP_READ);
}
}
selectedKeys.clear(); // 清楚全部的key
}
}
複製程式碼
客戶端程式碼:
// 開啟socket通道
SocketChannel socketChannel = SocketChannel.open();
// 設定為非阻塞方式
socketChannel.configureBlocking(false);
// 通過open()方法找到Selector
Selector selector = Selector.open();
// 註冊連線服務端socket動作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 連線
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
/* 資料緩衝區 */
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
if ((selector.select()) > 0) { // 選擇一組鍵,並且相應的通道已經準備就緒
Set<SelectionKey> selectedKeys = selector.selectedKeys();// 取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next(); // 取出每一個key
if (key.isConnectable()) {
SocketChannel client = (SocketChannel) key.channel();
if (client.isConnectionPending()) {
client.finishConnect();
byteBuffer.clear();
// 向緩衝區中設定內容
byteBuffer.put(("isConnect,當前的時間為:" + new Date()).getBytes());
byteBuffer.flip();
// 輸出內容
client.write(byteBuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable() && key.isValid()) {
SocketChannel client = (SocketChannel) key.channel();
byteBuffer.clear();
// 讀取內容到緩衝區中
int readSize = client.read(byteBuffer);
if (readSize > 0) {
System.out.println("客戶端接受伺服器端資料:" + new String(byteBuffer.array(), 0, readSize));
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (key.isWritable() && key.isValid()) {
SocketChannel client = (SocketChannel) key.channel();
byteBuffer.clear();
// 向緩衝區中設定內容
byteBuffer.put(("nio文章學習很多!").getBytes());
byteBuffer.flip();
// 輸出內容
client.write(byteBuffer);
client.register(selector, SelectionKey.OP_READ);
}
}
selectedKeys.clear(); // 清楚全部的key
}
}
複製程式碼
程式執行結果截圖:
**說明:**上面僅僅是一個demo,其實使用nio開發複雜度很高的,需要考慮:鏈路的有效性校驗機制(安全認證、半包和粘包等)、鏈路的斷連重連機制(網路閃斷重連)、可靠性設計(心跳檢測,訊息重發、黑白名單)以及可擴充套件性的考慮等等,這些都是很複雜,那裡搞不好就容易出錯,看netty 權威指南的時候 記得作者他們那個時候還沒有netty,經常出現一些莫名問題需要進行解決,而很多問題netty已經幫我們解決了,所以有必要好好看看netty了(目前作者也在看netty權威指南,唯一不爽的時候,裡面大量程式碼,習慣用工具檢視程式碼(編輯器檢視程式碼變色,可以跳轉等),求netty權威指南程式碼地址,看書裡面程式碼特別變扭!,謝謝)
簡單聊幾句AIO
雖然NIO在網路操作中提供了非阻塞方法,但是NIO的IO行為還是同步的,對於NIO來說,我們的業務執行緒是在IO操作準備好時,才得到通知,接著就有這個執行緒自行完成IO操作,但是IO操作的本身其實還是同步的。
AIO是非同步IO的縮寫,相對與NIO來說又進了一步,它不是在IO準備好時再通知執行緒,而是在IO操作完成後在通知執行緒,所以AIO是完全不阻塞的,我們的業務邏輯看起來就像一個回撥函式了。
**備註:**AIO就是簡單提提,NIO還沒有搞明白,後續還有一到二篇左右與NIO內容相關,主要談談一些select、poll、epoll、零拷貝等一些內容,如果有關零拷貝比較好的資料,歡迎在留言區進行留言,讓零度也學習下,系統的瞭解下,謝謝!!!
如果讀完覺得有收穫的話,歡迎點贊、關注、加公眾號【匠心零度】。
個人公眾號,歡迎關注,查閱更多精彩歷史!!!