1. Mô hình IO & Java IO
Unix cung cấp cho các lập trình viên năm mô hình IO cơ bản sau:
- chặn io: chặn io
- io không chặn: io không chặn
- Đa kênh I/O: đa kênh io
- I/O điều khiển bằng tín hiệu: io điều khiển bằng tín hiệu
- I/O không đồng bộ: io không đồng bộ
Nhưng những gì chúng ta thường nói đến nhiều nhất trong công việc là chặn, không chặn, đồng bộ và không đồng bộ.
1. Chặn và không chặn, đồng bộ và không đồng bộ
-
Cuộc gọi bị chặn có nghĩa là luồng hiện tại sẽ bị tạm dừng trước khi kết quả cuộc gọi được trả về. Luồng gọi sẽ không trả về cho đến khi nhận được kết quả. Cuộc gọi không chặn có nghĩa là cuộc gọi sẽ không chặn luồng hiện tại cho đến khi kết quả chưa có ngay lập tức.
Bạn có thể coi hàng đợi phía trên giống như một tủ đựng đồ mang đi.
Phương pháp put là để người giao hàng đặt đồ ăn. Nếu sức chứa không đủ, anh ta sẽ tiếp tục chờ người dùng khác lấy đồ ăn. Đây là chặn.
Phương pháp chào hàng cũng giống như người giao hàng đặt hàng, nhưng khi thấy không đủ hàng, họ trả về lỗi rồi thực hiện hành động khác, chẳng hạn như gọi bạn xuống lấy hàng.
-
Đồng bộ và không đồng bộ liên quan đến cơ chế truyền đạt thông điệp. Đồng bộ có nghĩa là cuộc gọi sẽ không trả về cho đến khi có được kết quả. Không đồng bộ có nghĩa là sau khi cuộc gọi được bắt đầu, cuộc gọi sẽ trả về trực tiếp.
Một trong những chức năng của phần mềm trung gian hàng đợi tin nhắn là không đồng bộ. Người gửi trả về ngay sau khi gửi tin nhắn, mà không cần chờ người dùng xử lý tin nhắn.
Đồng bộ hóa có nghĩa là khi bạn gọi cho người giao hàng để hỏi đồ ăn ở đâu, bạn không cúp máy cho đến khi người giao hàng cho bạn biết.
Không đồng bộ có nghĩa là bạn gửi tin nhắn cho người giao hàng trên ứng dụng giao đồ ăn và bạn có thể làm những việc khác ngay sau khi gửi tin nhắn.
Làm sao bạn biết được nơi nào có đồ ăn mang đi trong tình huống không đồng bộ?
-
thông báo.
Người giao hàng sẽ trả lời bạn thông qua nền tảng này.
-
Gọi lại.
Bạn đã đăng ký sự kiện gọi lại cho người giao hàng - sau khi nhận được tin nhắn, vui lòng gọi lại để thông báo, sau đó bạn kết thúc cuộc gọi và tiếp tục xử lý công việc của mình, nhưng sau khi nhận được tin nhắn, người giao hàng sẽ gọi lại để thực hiện cuộc gọi điện thoại.
2. Mô hình Unix io
Hoạt động io được chia thành hai bước:
-
Đang chờ dữ liệu sẵn sàng.
Ví dụ, khi đọc một tệp, bạn cần đợi đĩa quét dữ liệu cần thiết và dữ liệu đến bộ đệm hạt nhân.
-
Sao chép dữ liệu từ không gian hạt nhân sang không gian người dùng.
Đối với hoạt động IO đọc, dữ liệu không được sao chép trực tiếp vào bộ đệm của ứng dụng (không gian người dùng), trước tiên dữ liệu được sao chép vào bộ đệm của nhân hệ điều hành (không gian nhân), sau đó được sao chép từ bộ đệm của nhân hệ điều hành vào bộ đệm của ứng dụng.
2.1 chặn io
Đầu tiên, người dùng thực hiện lệnh gọi hệ thống, một lệnh ngắt được tạo ra, hệ điều hành chuyển sang trạng thái hạt nhân, sau đó hạt nhân hoàn tất việc chuẩn bị dữ liệu và sao chép dữ liệu từ không gian hạt nhân sang không gian người dùng, sau đó tiến trình ứng dụng tiếp tục chạy.
Việc chặn được đề cập ở đây có nghĩa là lệnh gọi hệ thống sẽ không trả về ngay lập tức mà cần phải chặn cho đến khi dữ liệu đã sẵn sàng và được sao chép vào không gian người dùng.
2.2 io không chặn
Có thể thấy rằng sự khác biệt so với chặn IO là trong quá trình chuẩn bị dữ liệu, ứng dụng liên tục thực hiện các cuộc gọi hệ thống để hỏi nhân hệ điều hành xem việc chuẩn bị dữ liệu đã hoàn tất hay chưa. Cuộc gọi hệ thống này sẽ không chặn cho đến khi việc chuẩn bị dữ liệu hoàn tất, nhưng sẽ trả về ngay lập tức.
Tuy nhiên, ở giai đoạn thứ hai, dữ liệu được sao chép từ không gian hạt nhân đến không gian người dùng, bị chặn. Quá trình này thường diễn ra nhanh vì bộ điều khiển DMA đã hoàn tất việc truyền dữ liệu từ đĩa đến bộ nhớ và chỉ cần sao chép đến không gian người dùng.
2.3 Đa kênh I/O
Có thể thấy rằng quá trình ghép kênh IO tương tự như quá trình chặn IO, thậm chí còn có thêm một lệnh gọi hệ thống nữa. Vậy ý nghĩa của việc ghép kênh IO là gì?
Giả sử tiến trình hiện tại của chúng ta là một chương trình máy chủ, có nhiều ios mạng cần được xử lý, chúng ta cần nhiều luồng để xử lý nhiều ios mạng và nhiều luồng bị chặn trong lệnh gọi hệ thống, đây là sự lãng phí tài nguyên luồng.
Ưu điểm của ghép kênh io là bạn có thể sử dụng một luồng để giám sát nhiều kênh io. Luồng này bị chặn trên lệnh gọi hệ thống select. Khi bất kỳ kênh io nào có thể đọc được, luồng sẽ được đánh thức, sau đó dữ liệu sẽ được sao chép và xử lý, do đó tiết kiệm tài nguyên luồng.
2.4 tín hiệu điều khiển I/O
Có thể thấy rằng IO điều khiển bằng tín hiệu không chặn trong giai đoạn chuẩn bị dữ liệu. Khi hệ điều hành hoàn tất việc chuẩn bị dữ liệu, nó sẽ gửi tín hiệu để thông báo cho quy trình người dùng rằng một sự kiện đã xảy ra. Quy trình người dùng cần viết một hàm xử lý tín hiệu tương ứng, chặn bản sao dữ liệu hạt nhân trong hàm xử lý tín hiệu và xử lý dữ liệu sau khi bản sao hoàn tất.
2.5 I/O không đồng bộ
Bốn mô hình trên sẽ bị chặn khi sao chép dữ liệu từ không gian hạt nhân sang không gian người dùng, điều này có nghĩa là ít nhất bước thứ hai cần phải chờ đồng bộ để hệ điều hành hoàn tất quá trình sao chép.
Mô hình IO không đồng bộ giải quyết vấn đề này. Ứng dụng chỉ cần thông báo cho kernel về đối tượng socket cần đọc và địa chỉ nhận dữ liệu. Toàn bộ quá trình được kernel hoàn thành độc lập, bao gồm cả việc sao chép dữ liệu từ không gian kernel sang không gian người dùng. Sau khi quá trình sao chép hoàn tất, tiến trình người dùng sẽ được thông báo bằng tín hiệu.
2.Mô hình IO trong Java
Kết hợp các phương pháp chặn, không chặn, đồng bộ và không đồng bộ.
-
Chặn io đồng bộ.
Đây là BIO trong Java.
-
IO đồng bộ không chặn.
Đây là NIO trong Java. NIO trong Java được triển khai thông qua ghép kênh IO.
-
Io không đồng bộ không chặn.
Đây là AIO trong Java. AIO trong Java cũng được triển khai thông qua ghép kênh IO, cho thấy giao diện không đồng bộ.
2. Java TIỂU SỬ
Phần sau đây thảo luận về những thiếu sót trong việc triển khai BIO của lập trình Socket trong Java.
public static void main(String[] args) throws IOException { ExecutorService threadPool = new ThreadPoolExecutor(10,10,100, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100)); // 1 Tạo một máy chủ socket lắng nghe trên cổng tcp 1111 ServerSocket serverSocket = new ServerSocket(1111); // 2 Chặn chấp nhận kết nối từ máy khách while (true) { //Bước này sẽ chặn cho đến khi máy khách kết nối Socket socket = serverSocket.accept(); System.out.println(socket.getRemoteSocketAddress() + "Đã kết nối với máy chủ"); // 3 Để không ảnh hưởng đến các kết nối tiếp theo đến, hãy sử dụng nhiều luồng để xử lý kết nối threadPool.execute(() -> process(socket)); } } private static void process(Socket socket) { try (OutputStream out = socket.getOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = socket.getInputStream().read(buffer)) > 0) { System.out.println(socket.getRemoteSocketAddress() + "Gửi dữ liệu: " + new String(buffer, 0, len)); out.write(buffer, 0, len); } } catch (Ngoại lệ e) { e.printStackTrace(); } }
Đoạn mã trên thực hiện rằng nếu máy khách yêu cầu, yêu cầu của máy khách sẽ được ghi lại nguyên vẹn. Bạn có thể thấy rằng để hiện thực hóa máy chủ hỗ trợ nhiều kết nối máy khách, chúng ta sử dụng một nhóm luồng.
Đầu tiên, Socket socket = serverSocket.accept(), bước này sẽ chặn cho đến khi máy khách kết nối (bước này không quan trọng, nó thậm chí còn tránh luồng chính quay liên tục).
Thứ hai, thao tác lấy luồng đầu vào và đầu ra và ghi lại chúng trong phương thức xử lý cũng bị chặn. Bước này yêu cầu sử dụng các lệnh gọi hệ thống do hệ điều hành cung cấp để đọc dữ liệu từ card mạng hoặc ổ cứng vào không gian hạt nhân, sau đó sao chép dữ liệu từ không gian hạt nhân vào không gian người dùng. Chỉ khi đó, chương trình Java của chúng ta mới có thể thực hiện các thao tác đọc và ngược lại đối với các thao tác ghi.
Vì các phương thức đọc và ghi đang chặn, chúng cần phải chặn cho đến khi lệnh gọi hệ thống hoàn tất. Chương trình của chúng tôi chặn và chờ một cách ngu ngốc, vì vậy chúng tôi sử dụng một nhóm luồng, hy vọng rằng một luồng có thể xử lý một yêu cầu của khách hàng và việc chặn chỉ chặn các luồng trong nhóm luồng. Tuy nhiên, phần bị chặn trong phương thức xử lý sẽ được phản ánh trong các luồng của nhóm luồng của chúng ta, nghĩa là có một số luồng trong nhóm luồng bị chặn trong các hàm đọc và ghi.
Ưu điểm của mô hình này:
- Đơn giản và trực tiếp, cho phép các nhà phát triển tập trung vào việc viết logic kinh doanh của quy trình
- Không cần phải lo lắng quá nhiều về tình trạng quá tải của hệ thống, giới hạn dòng điện và các vấn đề khác. Bản thân nhóm luồng là một kênh tự nhiên có thể đệm một số kết nối hoặc yêu cầu mà hệ thống không thể xử lý.
- Sử dụng đa luồng để tận dụng sức mạnh của CPU đa lõi. Khi một luồng bị chặn, CPU có thể chuyển các lát cắt thời gian sang các luồng khác.
Nhược điểm của mô hình này:
- Nó phụ thuộc rất nhiều vào luồng. Luồng là tài nguyên quý giá. Mặc dù nhóm luồng được sử dụng để tái sử dụng, chúng ta không thể mở luồng vô thời hạn khi có một lượng lớn yêu cầu đến. Nhiều luồng bị tạm dừng và việc đánh thức cũng sẽ dẫn đến việc chuyển đổi ngữ cảnh thường xuyên và giảm việc sử dụng CPU.
- Bản thân các luồng chiếm rất nhiều bộ nhớ, quá nhiều luồng sẽ gây nguy hiểm cho bộ nhớ jvm
Vậy làm sao để giải quyết vấn đề trên? Chúng ta có thể giải phóng các luồng để chúng không bị chặn trong quá trình đọc và ghi, để chúng có thể đọc nếu có thể và tiếp tục xử lý các socket khác nếu không thể không?
3. JavaNIO
Nhìn lại bức ảnh này, chúng tôi đã nói ở trên rằng chúng tôi giải phóng các luồng để chúng không bị chặn trong quá trình đọc và ghi. Nếu chúng có thể đọc, chúng có thể đọc. Nếu chúng không thể đọc, chúng có thể tiếp tục xử lý các socket khác. Đây không phải là phương pháp không chặn ở trên sao? Chúng tôi hy vọng rằng lệnh gọi hệ thống có thể trả về ngay lập tức thay vì chặn.
NIO trong Java triển khai phương pháp xử lý không chặn đồng bộ dựa trên ghép kênh IO.
Công khai void void Main (String [] args) ném IOException, InterruptedException {// 1 Tạo một bộ chọn để nghe cho nhiều tin nhắn IO mô tả tệp '// Bộ chọn đóng vai trò thông báo quan trọng. Máy chủ được kết nối bởi máy khách // mỗi kết nối máy khách cũng sẽ được đăng ký và sẽ có dữ liệu được gửi bởi bộ chọn chọn máy khách = selection.open (); .0.1 ", 1111)); // Định cấu hình dưới dạng máy chủ không chặn ( //Quan tâm đến sự kiện accept, serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 3 Bước này đang chặn, dựa trên select poll trong ghép kênh io, epoll // Bạn có thể đặt sự kiện wait tại đâyif (selector.select() == 0) { continue; } // 4 Nếu có ít nhất một IO có thông báo, thì tập hợp không rỗngSet selectionKeys = selector.selectedKeys(); for (SelectionKey key : selectionKeys) { if (key.isAcceptable()) { System.out.println("Kết nối máy khách"); // Vì chúng ta chỉ đăng ký serverSocketChannel có thể chấp nhận, nên chúng ta có thể sử dụng chuyển đổi mạnh hereSocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept(); socketChannel.configureBlocking(false); // 5 ByteBuffer.clear(); if (socketChannel.read(byteBuffer) <= 0) { continue; } byteBuffer.flip(); byte[] b = new byte[byteBuffer.limit()]; byteBuffer.get(b); System.out.println(key + " Dữ liệu đang đến: " + new String(b)); byteBuffer.clear(); byteBuffer.put(b); byteBuffer.flip(); socketChannel.write(byteBuffer); } } // 8 Việc dọn dẹp khóa của mỗi kênh là rất quan trọng để chỉ ra rằng kênh đó đã được xử lý, nếu không, kênh đó sẽ được chọn vào lần tiếp theo selectionKeys.clear(); } }
Chọn đang chặn. Cho dù là thông qua thông báo từ hệ điều hành (epoll) hay thăm dò liên tục (select, poll), chức năng này đang chặn. Nó cũng hỗ trợ chế độ chặn hết thời gian chờ. Đây là biểu hiện của một luồng giám sát nhiều IO. Miễn là một sự kiện đã sẵn sàng, select sẽ trả về.
socketChannel.configureBlocking(false) đặt socketChannel thành non-blocking. Các hoạt động đọc và ghi của nó là non-blocking. Nghĩa là, nếu nó không thể đọc, hàm đọc trả về -1, điều này sẽ cho phép luồng hiện tại duyệt qua các sự kiện sẵn sàng khác thay vì chờ đợi một cách ngu ngốc. Đây là hiện thân của non-blocking IO.
4. JavaAIO
// Phân phối tham số accept serverChannel.accept(attachment, this); System.out.println(Thread.currentThread() + "A client has connected" + channel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer, null, new CompletionHandler() { @SneakyThrows @Override public void completed(AsynchronousSocketChannel channel, Object attachment) { // Phân phối tham số accept serverChannel.accept(attachment, this); System.out.println(Thread.currentThread() + "A client has connected" + channel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer, null, new CompletionHandler() { @SneakyThrows @Override public void completed(Integer len, ByteBuffer attachment) { // Đăng ký đệ quy đọc channel.read(buffer, null, this); buffer.flip(); System.out.println(channel.getRemoteAddress() + ":" + new String(buffer.array(), 0, len)); buffer.clear(); channel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer attachment) { } }); } @Override public void failed(Throwable exc, Object attachment) { } }); Thread.sleep(Integer.MAX_VALUE); }
Trong AIO, tất cả các kênh được tạo sẽ được đăng ký và lắng nghe trực tiếp trên HĐH. Khi yêu cầu IO xảy ra, hệ điều hành sẽ nhận, chuẩn bị và sao chép dữ liệu trước, sau đó thông báo cho chương trình đang lắng nghe kênh tương ứng để xử lý dữ liệu.
Khi kết nối máy khách xuất hiện, nó cũng sẽ được đăng ký với bộ chọn trước, nhưng yêu cầu I/O của máy khách sẽ được hệ điều hành xử lý trước và một luồng sẽ được chỉ định để xử lý yêu cầu đó chỉ sau khi nhân hoàn tất việc sao chép dữ liệu. Điều này khác với BIO và NIO, vẫn bị chặn khi sao chép dữ liệu từ hạt nhân sang chế độ người dùng.
Cuối cùng, bài viết này về JavaBIO, NIO và AIO kết thúc tại đây. Nếu bạn muốn biết thêm về JavaBIO, NIO và AIO, vui lòng tìm kiếm các bài viết trên CFSDN hoặc tiếp tục duyệt các bài viết liên quan. Tôi hy vọng bạn sẽ ủng hộ blog của tôi trong tương lai! .
Tôi là một lập trình viên xuất sắc, rất giỏi!