阻塞與同步
1、阻塞和非阻塞
阻塞和非阻塞是進程在訪問數據的時候,數據是否準備就緒的一種處理方式,當數據沒有準備的時候一直等待,直到有數據返回,否則一直等待在那裏。
非阻塞:當我們的進程訪問我們的數據緩衝區的時候,如果數據沒有準備好則直接返回,不會等待。如果數據已經準備好,也直接返回。
如果用一個燒水例子解釋這個問題:
阻塞:就相當於水壺放在放到火上,立等水開才能知道水是否開了。
非堵塞:就相當於把水壺放在火上,隨時去看,都知道水開還是沒開而不是要等到水開了,才能告知結論。如果採用這種方案就是,可以看電視時不時去看,水是否開了。(同步阻塞)
NIO同步非堵塞IO:
當此此線程中沒有數據返回的時候,可以先處理其他線程的任務,不用卡在此線程一直等待數據返回。
2、同步(Synchronization)和異步(Async)的方式
同步和異步都是基於應用程序私操作系統處理IO事件所採用的方式,比如同步:是應用程序要直接參與IO讀寫的操作。異步:所有的IO讀寫交給搡作系統去處理,應用程序只需要等待通知。
如果用一個燒水例子解釋這個問題:
同步:就相當於水壺放在放到火上,水開的時候並不會通知水開了,而需要一個線程去看水是否開了。
異步:當水開了,會通知我們水已經開了。
異步非阻塞IO:
獲取數據的時候,無論是否有數據都立即返回,並且在內核在緩衝區裏填入了數據之後自動調用內存拷貝。
BIO代碼演示
BIO客戶端:
public class BIOClient {
private static Charset charset = Charset.forName("UTF-8");
public static void main(String[] args) throws Exception {
//開啓一個8080端口的線程
Socket s = new Socket("localhost", 8080);
OutputStream out = s.getOutputStream();
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入:");
String msg = scanner.nextLine();
out.write(msg.getBytes(charset));
scanner.close();
s.close();
}
}
BIO客戶端:
bio由於Socket request = serverSocket.accept(); 獲取連接和
InputStream inputStream = request.getInputStream(); 從連接獲取流是阻塞的故,當有一個請求,只能創建一個線程去處理,線程一直阻塞利用率並不高。創建的線程過多導致服務器的新能下降明顯。
public class BIOServer {
private static ExecutorService threadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服務器啓動成功");
while (!serverSocket.isClosed()) {
//獲取socket 連接數阻塞的
Socket request = serverSocket.accept();
System.out.println("收到新連接 : " + request.toString());
threadPool.execute(() -> {
try {
// 接收數據、打印(從連接中獲取數據同樣是阻塞的)
InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg;
while ((msg = reader.readLine()) != null) {
if (msg.length() == 0) {
break;
}
System.out.println(msg);
}
System.out.println("收到數據,來自:"+ request.toString());
// 響應結果 200
OutputStream outputStream = request.getOutputStream();
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
outputStream.write("Content-Length: 11\r\n\r\n".getBytes());
outputStream.write("Hello World".getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
request.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
serverSocket.close();
}
}
產生結果:
NIO代碼演示
1、緩衝區Buffer
緩衝區實際上是一個容器對象,可以看做一個功能增強的數組(有位置、固定容量、標記),在NIO庫中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的; 在寫入數據時,它也是寫入到緩衝區中的;而在面向流I/O系統中,所有數據都是直接寫入或者直接將數據讀取到Stream對象中。
容量(capacity):
緩衝區能夠容納的數據元素的最大數量。這一容量在緩衝區創建時被設定,並且永遠不能被改變
上界(limit):
緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數
位置(position):
下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新
標記(mark):
下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新一個備忘位置。
2、通道Channel
通道是一個對象,通過它可以讀取和寫入數據,當然了所有數據都通過Buffer對象來處理。我們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩衝區。同樣不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。
3、選擇器
SelectableChannel 可被註冊到 Selector 對象上,同時可以指定對那個選擇器而言,哪種操作是感興趣的,當發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言,只能被註冊一次,通道在被註冊到一個選擇器上之前,必須先設置爲非阻塞模式,通過調用通道的configureBlocking(false)方法即可。這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式,而套接字通道都可以。
在nio的代碼寫的過程中有很多需要注意的地方:
https://www.cnblogs.com/pingh/p/3224990.html
下面設置關於通過nio來演示客戶端與服務端的傳輸
客戶端
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
while (!socketChannel.finishConnect()) {
// 沒連接上,則一直等待
Thread.yield();
}
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入:");
// 發送內容
String msg = scanner.nextLine();
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 讀取響應
System.out.println("收到服務端響應:");
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
if (requestBuffer.position() > 0){
break;
}
}
//nio 切換模式
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
scanner.close();
socketChannel.close();
}
}
服務端:
此處一個selector監聽所有事件,演示
public class NIOServerV2 {
public static void main(String[] args) throws Exception {
// 1. 創建網絡服務端ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 設置爲非阻塞模式
serverSocketChannel.configureBlocking(false);
// 2. 構建一個Selector選擇器,並且將channel註冊上去
Selector selector = Selector.open();
// 將serverSocketChannel註冊到selector
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
// 對serverSocketChannel上面的accept事件感興趣(serverSocketChannel只能支持accept操作)
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
// 3. 綁定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
System.out.println("啓動成功");
while (true) {
// 用下面輪詢事件的方式.select方法有阻塞效果,直到有事件通知纔會有返回
selector.select();
// 獲取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍歷查詢結果
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
// 被封裝的查詢結果
SelectionKey key = iter.next();
iter.remove();
// 關注 Read 和 Accept兩個事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.attachment();
// 將拿到的客戶端連接通道,註冊到selector上面
SocketChannel clientSocketChannel = server.accept();
clientSocketChannel.configureBlocking(false);
clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
System.out.println("收到新連接 : " + clientSocketChannel.getRemoteAddress());
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.attachment();
try {
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
if (requestBuffer.position() > 0) break;
}
// 如果沒數據了, 則不繼續後面的處理
if(requestBuffer.position() == 0) continue;
//切換模式
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
System.out.println("收到數據,來自:" + socketChannel.getRemoteAddress());
// 響應結果 200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
} catch (IOException e) {
// e.printStackTrace();
// 取消事件訂閱
key.cancel();
}
}
}
selector.selectNow();
}
}
}
運行結果:
NIO多路複用模型演示
一個selector監聽所有事件,個線程處理所有請求事件,這裏通過多路複用來展示這個過程,這也是netty採用的方式,只是則是netty的簡化版本。
/**
* NIO selector 多路複用reactor線程模型
*/
public class NIOServerV3 {
abstract class EventLoop extends Thread {
Selector selector;
LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
/**
* Selector監聽到有事件後,調用這個方法
*/
public abstract void handler(SelectableChannel channel) throws Exception;
private EventLoop() throws IOException {
selector = Selector.open();
}
volatile boolean running = false;
@Override
public void run() {
// 輪詢Selector事件
while (running) {
try {
// 執行隊列中的任務
Runnable task;
while ((task = taskQueue.poll()) != null) {
task.run();
}
selector.select(1000);
// 獲取查詢結果
Set<SelectionKey> selected = selector.selectedKeys();
// 遍歷查詢結果
Iterator<SelectionKey> iter = selected.iterator();
while (iter.hasNext()) {
// 被封裝的查詢結果
SelectionKey key = iter.next();
iter.remove();
int readyOps = key.readyOps();
// 關注 Read 和 Accept兩個事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
try {
SelectableChannel channel = (SelectableChannel) key.attachment();
channel.configureBlocking(false);
handler(channel);
if (!channel.isOpen()) {
key.cancel(); // 如果關閉了,就取消這個KEY的訂閱
}
} catch (Exception ex) {
key.cancel(); // 如果有異常,就取消這個KEY的訂閱
}
}
}
selector.selectNow();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private SelectionKey register(SelectableChannel channel) throws Exception {
FutureTask<SelectionKey> futureTask = new FutureTask<>(() -> channel.register(selector, 0, channel));
taskQueue.add(futureTask);
return futureTask.get();
}
private void doStart() {
if (!running) {
running = true;
start();
}
}
}
private ServerSocketChannel serverSocketChannel;
// 1、創建多個線程 - acceptor
private EventLoop[] bossThreads = new EventLoop[1];
// 2、創建多個線程 - io
private EventLoop[] workThreads = new EventLoop[1];
/**
* 初始化線程組
*/
private void newGroup() throws IOException {
// 創建Boss線程, 只負責處理serverSocketChannel
for (int i = 0; i < bossThreads.length; i++) {
bossThreads[i] = new EventLoop() {
AtomicInteger incr = new AtomicInteger(0);
@Override
public void handler(SelectableChannel channel) throws Exception {
// 只做請求分發,不做具體的數據讀取
ServerSocketChannel ch = (ServerSocketChannel) channel;
SocketChannel socketChannel = ch.accept();
socketChannel.configureBlocking(false);
// 收到連接建立的通知之後,分發給work線程繼續去讀取數據
int index = incr.getAndIncrement() % workThreads.length;
EventLoop workEventLoop = workThreads[index];
workEventLoop.doStart();
SelectionKey selectionKey = workEventLoop.register(socketChannel);
selectionKey.interestOps(SelectionKey.OP_READ);
System.out.println("收到新連接 : " + socketChannel.getRemoteAddress());
}
};
}
// 創建IO線程,負責處理客戶端連接以後socketChannel的IO讀寫
for (int i = 0; i < workThreads.length; i++) {
workThreads[i] = new EventLoop() {
@Override
public void handler(SelectableChannel channel) throws IOException {
// work線程只負責處理IO處理,不處理accept事件
SocketChannel ch = (SocketChannel) channel;
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (ch.isOpen() && ch.read(requestBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
if (requestBuffer.position() > 0){
break;
}
}
// 如果沒數據了, 則不繼續後面的處理
if (requestBuffer.position() == 0){
return;
}
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
System.out.println("收到數據,來自:" + ch.getRemoteAddress());
// 響應結果 200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
ch.write(buffer);
}
}
};
}
}
/**
* 初始化channel,並且綁定一個eventLoop線程
*
* @throws IOException IO異常
*/
private void initAndRegister() throws Exception {
// 1、 創建ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 2、 將serverSocketChannel註冊到selector
int index = new Random().nextInt(bossThreads.length);
bossThreads[index].doStart();
SelectionKey selectionKey = bossThreads[index].register(serverSocketChannel);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
}
/**
* 綁定端口
*
* @throws IOException IO異常
*/
private void bind() throws IOException {
// 1、 正式綁定端口,對外服務
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("啓動完成,端口8080");
}
public static void main(String[] args) throws Exception {
NIOServerV3 nioServerV3 = new NIOServerV3();
nioServerV3.newGroup();
nioServerV3.initAndRegister();
nioServerV3.bind();
int read = System.in.read();
System.out.println(read);
}
}