1、術語
併發 vs 並行
- 併發和並行是相關的概念,但有一些小的區別。併發意味着兩個或多個任務正在取得進展,即使它們可能不會同時執行。例如,這可以通過時間切片來實現,其中部分任務按順序執行,並與其他任務的部分混合。另一方面,當執行的任務可以真正同時進行時,就會出現並行
簡單説啓動一個線程在一個core上就是並行,啓動兩個線程在一個core上就是併發
異步 vs 同步
- 如果調用者在方法返回值或引發異常之前無法取得進展,則認為方法調用是同步的。另一方面,異步調用允許調用者在有限的步驟之後繼續進行,並且可以通過一些附加機制 (它可能是已註冊的回調、Future 或消息)來通知方法的完成
簡單來説Java API層來説的,如下 :
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
System.out.println("執行業務邏輯");
// 根據業務邏輯判斷給定返回
return true;
}
});
future.get(); // 同步API,必須等到返回
if(future.isDone()) {
future.get();// 異步API,只有執行完,再get結果
}
- 同步 API 可以使用阻塞來實現同步,但這不是必要的。CPU 密集型任務可能會產生類似 於阻塞的行為。一般來説,最好使用異步 API,因為它們保證系統能夠進行
非阻塞 vs 阻塞
- 如果一個線程的延遲可以無限期地延遲其他一些線程,這就是我們討論的阻塞。一個很好的例子是,一個線程可以使用互斥來獨佔使用一個資源。如果一個線程無限期地佔用資源(例如意外運行無限循環),則等待該資源的其他線程將無法進行。相反,非阻塞意味着沒有線程能夠無限期地延遲其他線程
- 非阻塞操作優先於阻塞操作,因為當系統包含阻塞操作時,系統的總體進度並不能得到很好的保證
死鎖 vs 飢餓 vs 活鎖
- 當多個線程在等待對方達到某個特定的狀態以便能夠取得進展時,就會出現死鎖。由於沒有其他線程達到某種狀態,所有受影響的子系統都無法繼續運行。死鎖與阻塞密切相關,因為線程能夠無限期地延遲其他線程的進程
- 在死鎖的情況下,沒有線程可以取得進展,相反,當有線程可以取得進展,但可能有一個或多個線程不能取得進展時,就會發生飢餓。典型的場景是一個調度算法,它總是選擇高優先級的任務而不是低優先級的任務。如果傳入的高優先級任務的數量一直足夠多,那麼低優先級任務將永遠不會完成
- 活鎖類似於死鎖,因為沒有線程取得進展。不同之處在於,線程不會被凍結在等待他人進展的狀態中,而是不斷地改變自己的狀態。一個示例場景是,兩個線程有兩個相同資源可用時。他們每一個都試圖獲得資源,但他們也會檢查對方是否也需要資源。 如果資源是由另一個線程請求的,他們會嘗試獲取該資源的另一個實例。在不幸的情 況下,兩個線程可能會在兩種資源之間“反彈”,從不獲取資源,但總是屈服於另一種資源
2、BIO vs NIO
BIO
serverSocket.accept(),這裏會阻塞
socket.getInputStream.read(),也會阻塞
雖然可以使用了線程池,因為read()方法的阻塞,其實線程池也是不能複用的,説白了,就是需要一個客户端一個線程進行服務
思考:那BIO就沒有使用場景了嗎?
其實不是,BIO在建立長連接的流式傳輸場景還是很有用的,比如説HDSF,客户端向DataNode傳輸數據使用的就是建立一個BIO的管道,流式上傳數據的。此時引入一個問題,那HDFS DataNode就不考慮到線程阻塞麼?是這樣的,其實要知道你不可能多個客户端上傳文件都是針對某個DataNode(NameNode會進行選擇DataNode),所以線程阻塞的壓力是會分攤的。NIO還是擅長小數據量的RPC請求,能接受百萬客户端的連接
NIO
NIO中有三個重要組件 : Buffer(ByteBuffer主要使用)、Channel(雙向通道,可讀可寫)和Selector(多路複用選擇器)
-
Buffer
常用的就是 ByteBuffer,緩衝池,可以作為channel寫的單位,也可以接受channel讀取的返回裏面重要的屬性 :position、capacity、flip、limit和hasRemain每個channel都需要記錄可能切分的消息,因為ByteBuffer不能被多個channel使用,因此需要為每個channel維護一個獨立的ByteBuffer。ByteBuffer不能太大,比如一個ByteBuffer 1M的話,需要支持百萬連接要1TB內存,因此需要設計大小可變的ByteBuffer
1、首先分配一個較小的buffer,比如4k,如果發現不夠的話,再分配8kb的buffer,將4kb buffer內容拷貝到8kb buffer,有點是消息連續容易處理,缺點是數據拷貝耗費性能
2、多個數組組成buffer,一個數組不夠,把多出來的內容寫入新的數組,缺點不連續解析複雜,有點避免了拷貝引起的性能損耗 -
FileChannel
FileChannel在同一個JVM中是線程安全的,多個線程寫也沒有問題,但是在不同的JVM中同時寫一個文件就會有問題需要的是 FileLock對文件進行加鎖,有獨佔鎖和共享鎖
channel.lock(0, Integer.MAX_VALUE, true),可以鎖一定的區間RandomAccessFile 可以支持讀寫文件,Channel 本身是支持讀寫的,只是看源頭是不是支持讀寫,比如説FileInputStream流獲取的channel只能支持讀,RandomAccessFile獲取的流支持讀寫
channel.force(true); 強制將os cache數據刷入到磁盤上
from.transferTo(position,count,dest);從from channel寫到dest channel,從position 開始寫,寫了count長度
比如説從本地文件向網絡中進行傳輸
to.transferFrom(src,position,count); 比如説從網絡中寫到本地文件,from就是從外界到,src讀取,寫到to中transferTo & transferFrom 底層使用的是零拷貝,零拷貝簡單來説其實不走應用層數據複製,但是也是有數據複製的,是在Linux內核層
-
Selector & SocketChannel
服務端 ServerSocketChannel
是通過 ServerSocketChannel和Selector來獲取多個連接,每個連接是一個SocketChannel
將 ServerSocketChannel 註冊到 Selector上,如果有連接,selector的select阻塞方法會有事件,生成SelectionKey,每個SelectionKey其實對應一個SocketChannelSelectionkey是可以attach對象的,也可以通過 Selectionkey 通過attachment進行對象的獲取,這很重要,一般會創建一個對象並和SelectioKey進行關聯
照樣是bind,監聽OP_ACCEPT,進行isAcceptable、read和write事件(write事件是一次沒有寫完畢,繼續要寫)
客户端 SocketChannel
是進行connect,監聽OP_CONNECT事件,isConnectable,read和write事件要注意,SelectionKey,每次迭代是需要刪除的,否則重複請求,但是已經處理,就會有問題
寫數據的時候一定要注意,最好不要 while(buffer.hasRemaining()) 一直寫,這樣會阻塞網絡帶寬的,影響讀取
寫一部分數據,然後關注SelectionKey.OP_WRITE事件,不斷selector.select()繼續寫,寫完畢取消寫事件的關注socketChannel.write(buffer); 寫一下,也不一定會把buffer中都寫完畢 if(buffer.hasRemaining()) { selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE); selectionKey.attach(buffer); }
3、零拷貝
傳統IO問題
比如説要將本地磁盤文件往網絡中寫,磁盤 -> 內核緩衝區 -> 用户緩衝區 -> socket緩衝區 -> 網卡
讀磁盤數據 : 用户態 -> 內核態
內核數據寫到用户緩衝區 : 內核態 -> 用户態
網卡寫數據 : 用户態 -> 內核態
4次數據複製,3次內核切換
通過DirectByteBuffer,MappedByteBuffer,為什麼快?
因為他使用direct buffer的方式讀寫文件內容,稱為內存映射。這種方式直接調用系統底層的緩存,沒有JVM和系統之間的複製操作,所以效率大大的提升
將堆外內存映射到JVM內存中直接訪問
減少一次數據拷貝,用户態與內核態的切換次數沒有減少
Linux2.4
Java調用transferTo,要從Java程序的用户態到內核態,磁盤 -> 內核緩衝區 -> 網卡,一次內核切換,兩次數據複製
4、Socket參數
SocketChannel參數
SO_RCVBUF和SO_SNDBUF : Socket參數,TCP數據接收緩衝區大小,發送和接受緩衝區,128kb或者256kb
CONNECT_TIMEOUT_MILLIS : 用户在客户端建立連接時,如果在指定毫秒內無法建立連接,會拋出timeout異常
TCP_NODELAY TCP參數,立即發送數據,默認值為Ture(關閉nagle算法)
SO_KEEPALIVE Socket參數,連接保活,默認值為False。啓用該功能時,TCP會主動探測空閒連接的有效性(2個小時)
SO_REUSEADDR : 其實就是比如説ServerSocketChannel連接關閉了,此時跟其他客户端的連接都處於一個timeout狀態,重啓Netty Server,如果設置了
SO_REUSEADDR 為 true,則會讓ServerSocketChannel重新地址端口綁定,否則失敗
ServerSocketChannel參數
SO_BACKLOG Socket參數,服務端接受連接的隊列長度,如果隊列已滿,客户端連接將被拒絕。默認值,Windows為200,其他為128
TCP三次握手是在ACCEPT之前發生的
1、第一次握手,client發送SYN到server,狀態修改為SYN_SEND,server收到,狀態修改為SYN_REVD,並將請求放入sync queu隊列
2、第二次握手,server回覆 SYN + ACK 給client,client收到,狀態修改為ESTABLISHED,併發送給ACK給server
3、第三次握手,server收到ack,狀態修改為 ESTABLISHED,將請求從sync queue放入accept queue
所以現在出現了半連接隊列和全連接隊列
在Centos Linux下對應着 /proc/sys/net/ipv4/tcp_max_syn_backlog(512),/proc/sys/net/core/somaxconn(128)
SO_BACKLOG 設置的是全連接
TCP SYNC FLOOD惡意DOS攻擊方式就是建立大量的半連接狀態的請求,然後丟棄
如感興趣,點贊加關注哦!