Heim >Java >javaLernprogramm >Detaillierte Erläuterung des I/O-Modells in JAVA (mit Beispielen)
Vielleicht fällt es vielen Freunden etwas schwer, NIO zu lernen, und viele Konzepte darin sind nicht so klar. Bevor wir mit der Java-NIO-Programmierung beginnen, besprechen wir heute einige Grundkenntnisse: E/A-Modell. Der folgende Artikel beginnt mit den Konzepten von Synchronisation und Asynchronität, erklärt dann den Unterschied zwischen Blockierung und Nichtblockierung, stellt dann den Unterschied zwischen blockierender E/A und nicht blockierender E/A vor, stellt dann den Unterschied zwischen synchroner E/A und asynchroner E/A vor und führt dann ein 5 IO-Modelle und stellt schließlich zwei Designmodelle vor (Reactor und Proactor), die sich auf Hochleistungs-IO-Design beziehen
Das Folgende ist die Inhaltsübersicht dieses Artikels:
1. Was ist Synchronisation? Was ist asynchron?
2. Was ist Blockieren? Was ist nicht blockierend?
3. Was blockiert IO? Was ist nicht blockierendes IO?
4. Was ist synchrones IO? Was ist asynchrones IO?
5. Fünf IO-Modelle
6. Zwei Hochleistungs-IO-Entwurfsmuster
1. Was ist Synchronisation? Was ist asynchron?
Die Konzepte der Synchronisation und Asynchronität gibt es schon seit langem, und im Internet gibt es viele Meinungen über Synchronisation und Asynchronität. Das Folgende ist mein persönliches Verständnis:
Synchronisierung bedeutet: Wenn mehrere Aufgaben oder Ereignisse auftreten, müssen diese Aufgaben oder Ereignisse einzeln ausgeführt werden. Die Ausführung eines Ereignisses oder einer Aufgabe löst den gesamten Prozess aus vorübergehend warten. Diese Ereignisse können nicht gleichzeitig ausgeführt werden.
Asynchron bedeutet: Wenn mehrere Aufgaben oder Ereignisse gleichzeitig ausgeführt werden, und die Ausführung eines Ereignisses oder einer Aufgabe nicht dazu führt, dass der gesamte Prozess wartet vorübergehend.
Dies ist synchron und asynchron. Um ein einfaches Beispiel zu geben: Wenn es eine Aufgabe gibt, die zwei Unteraufgaben A und B enthält. Bei der Synchronisierung kann B bei der Ausführung von A nur warten, bis A abgeschlossen ist, bevor B ausgeführt werden kann. Bei der asynchronen Ausführung können A und B ausgeführt werden gleichzeitig, und B muss nicht darauf warten, dass A die Ausführung beendet, sodass die Ausführung von A nicht dazu führt, dass die gesamte Aufgabe vorübergehend wartet.
Wenn Sie es immer noch nicht verstehen, können Sie zuerst die folgenden zwei Codeteile lesen:
void fun1() { } void fun2() { } void function(){ fun1(); fun2() ..... ..... }
Dieser Codeteil ist eine typische Synchronisierung in der Methodenfunktion, fun1 wird nachfolgend ausgeführt fun2 kann nicht ausgeführt werden, fun2 muss warten, bis fun1 die Ausführung abgeschlossen hat, bevor es ausgeführt werden kann.
Dann schauen Sie sich den folgenden Code an:
void fun1() { } void fun2() { } void function(){ new Thread(){ public void run() { fun1(); } }.start(); new Thread(){ public void run() { fun2(); } }.start(); ..... ..... }
Dieser Code ist ein typischer asynchroner Code. Die Ausführung von fun1 hat keinen Einfluss auf die Ausführung von fun2, und die Ausführung von fun1 und fun2 verursacht keinen Der nachfolgende Ausführungsprozess wartet vorübergehend.
Tatsächlich sind Synchronisation und Asynchronität sehr weit gefasste Konzepte, und ihr Fokus liegt darauf, ob das Auftreten oder die Ausführung eines Ereignisses dazu führt, dass der gesamte Prozess vorübergehend wartet, wenn mehrere Aufgaben und Ereignisse auftreten . Ich denke, dass eine Analogie zwischen Synchronisation und Asynchronität mit dem synchronisierten Schlüsselwort in Java hergestellt werden kann. Wenn mehrere Threads gleichzeitig auf eine Variable zugreifen, ist der Zugriff jedes Threads auf die Variable ein Ereignis. Während ein Thread auf die Variable zugreift, muss er asynchron sein , müssen mehrere Threads nicht einzeln auf die Variable zugreifen, sondern können gleichzeitig darauf zugreifen.
Daher bin ich persönlich der Meinung, dass Synchronisation und Asynchronität auf viele Arten ausgedrückt werden können. Der Schlüssel zum Erinnern liegt jedoch darin, dass beim Auftreten mehrerer Aufgaben und Ereignisse unabhängig davon, ob das Auftreten oder die Ausführung eines Ereignisses den gesamten Prozess auslöst vorübergehend warten. Im Allgemeinen kann Asynchronität durch Multithreading erreicht werden. Denken Sie jedoch daran, Multithreading nicht mit Asynchronität gleichzusetzen. Asynchronität ist nur ein Makromuster, und die Verwendung von Multithreading zur Erzielung von Asynchronität ist nur ein Mittel, und eine asynchrone Implementierung kann ebenfalls erreicht werden durch Mehrfachverarbeitung.
2. Was ist Blockieren? Was ist nicht blockierend?
Der Unterschied zwischen Synchronisation und Asynchronität wurde bereits früher vorgestellt. In diesem Abschnitt werfen wir einen Blick auf den Unterschied zwischen Blockierung und Nichtblockierung.
Blockierung ist : Wenn ein Ereignis oder eine Aufgabe ausgeführt wird, wird eine Anforderungsoperation ausgegeben, aber da die für die Anforderungsoperation erforderlichen Bedingungen nicht erfüllt sind, wird sie immer dort warten. bis die Bedingungen erfüllt sind;
Nicht blockierend bedeutet: Wenn ein Ereignis oder eine Aufgabe ausgeführt wird, wird eine Anforderungsoperation ausgegeben. Es wird sofort eine Flag-Nachricht zurückgegeben, um darüber zu informieren, dass die Bedingung nicht erfüllt ist, und es wird nicht ewig dort warten.
Das ist der Unterschied zwischen Blockieren und Nichtblockieren. Das heißt, der Hauptunterschied zwischen Blockierung und Nichtblockierung besteht darin, ob bei der Anforderung einer Operation, wenn die Bedingungen nicht erfüllt sind, ewig gewartet wird oder eine Flag-Nachricht zurückgegeben wird. Geben Sie ein einfaches Beispiel:
Wenn ich den Inhalt einer Datei lesen möchte und zu diesem Zeitpunkt kein lesbarer Inhalt in der Datei vorhanden ist, wird sie immer dort auf die Synchronisierung warten. Bis die Datei lesbaren Inhalt enthält, wird bei Nichtblockierung direkt eine Flag-Meldung zurückgegeben, um darüber zu informieren, dass die Datei keinen lesbaren Inhalt enthält.
Einige Freunde im Internet setzen Synchronisation und Asynchronität mit Blockieren bzw. Nichtblockieren gleich. Tatsächlich handelt es sich um zwei völlig unterschiedliche Konzepte. Beachten Sie, dass es für das Verständnis des nachfolgenden IO-Modells sehr wichtig ist, den Unterschied zwischen diesen beiden Konzeptsätzen zu verstehen.
Der Fokus von Synchronisation und Asynchronität liegt darauf, ob die Ausführung einer Aufgabe dazu führt, dass der gesamte Prozess vorübergehend wartet, während mehrere Aufgaben ausgeführt werden.
Der Fokus von Blockieren und Nichtblockieren liegt beim Ausgeben einer Anforderung Wenn während des Betriebs die Bedingungen für den Vorgang nicht erfüllt sind, wird eine Flag-Nachricht zurückgegeben, um darüber zu informieren, dass die Bedingungen nicht erfüllt sind.
Das Verständnis von Blockierung und Nichtblockierung kann analog zur Thread-Blockierung verstanden werden. Wenn ein Thread eine Anforderungsoperation ausführt und die Bedingung nicht erfüllt ist, wird er blockiert, dh er wartet darauf, dass die Bedingung erfüllt ist traf.
3. Was ist blockierende E/A? -Blocking IO Schauen Sie sich vor dem Blockieren von IO zunächst an, wie der nächste spezifische IO-Vorgangsprozess ausgeführt wird. Im Allgemeinen umfassen E/A-Vorgänge: Lesen und Schreiben auf die Festplatte, Lesen und Schreiben auf den Socket sowie Lesen und Schreiben auf die Peripheriegeräte.
Werfen wir zunächst einen Blick auf die Definitionen von synchronem IO und asynchronem IO im Buch „Unix Network Programming“: Eine synchrone E/A-Operation führt dazu, dass der anfordernde Prozess blockiert wird, bis diese E/A-Operation abgeschlossen ist.
Eine asynchrone E/A-Operation führt nicht dazu, dass der anfordernde Prozess blockiert wird.
Aus der wörtlichen Bedeutung geht hervor: synchrones IO bedeutet, dass, wenn ein Thread einen IO-Vorgang anfordert, der Thread blockiert wird, bevor der IO-Vorgang abgeschlossen ist;
Asynchrones IO bedeutet, wenn Ein Thread fordert die Ausführung von E/A-Vorgängen an, die nicht dazu führen, dass der Anforderungsthread blockiert wird. A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.<br> An asynchronous I/O operation does not cause the requesting process to be blocked.
Der Hauptunterschied zwischen synchronem IO und asynchronem IO spiegelt sich darin wider, ob die Datenkopierphase vom Benutzerthread oder vom Kernel abgeschlossen wird. Daher muss asynchrone E/A eine zugrunde liegende Unterstützung vom Betriebssystem haben
. Beachten Sie, dass synchrone E/A und asynchrone E/A zwei verschiedene Konzepte sind als blockierende E/A und nicht blockierende E/A. Blockierende E/A und nicht blockierende E/A spiegeln sich darin wider, dass der Benutzerthread immer noch eine Flag-Nachricht erhält, wenn der Benutzer eine E/A-Operation anfordert und die Daten nicht bereit sind, wenn er auf die Daten wartet bereit sein. Mit anderen Worten, blockierende E/A und nicht blockierende E/A spiegeln sich in der ersten Phase der E/A-Operation wider, nämlich in der Art und Weise, wie sie verarbeitet wird, wenn überprüft wird, ob die Daten bereit sind.5. Fünf I/O-Modelle
Im Buch „Unix Network Programming“ werden jeweils fünf IO-Modelle erwähnt : blockierende E/A, nicht blockierende E/A, gemultiplexte E/A, signalgesteuerte E/A und asynchrone E/A. Lassen Sie uns die Gemeinsamkeiten bzw. Unterschiede dieser 5 IO-Modelle vorstellen.
1. Blockierendes IO-Modell
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
典型的阻塞IO模型的例子为:
data = socket.read();
如果数据没有就绪,就会一直阻塞在read方法。
2.非阻塞IO模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
典型的非阻塞IO模型一般如下:
while(true){ data = socket.read(); if(data!= error){ 处理数据 break; } }
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
3.多路复用IO模型
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
也许有朋友会说,我可以采用 多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
4.信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
5.异步IO模型
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
Mit anderen Worten: Im asynchronen E/A-Modell blockiert keine Phase der E/A-Operation den Benutzerthread. Beide Phasen werden vom Kernel automatisch abgeschlossen, und dann wird ein Signal gesendet, um den Benutzerthread darüber zu informieren, dass die Operation durchgeführt wurde vollendet. Für bestimmte Lese- und Schreibvorgänge ist es nicht erforderlich, die IO-Funktion im Benutzerthread erneut aufzurufen. Dies unterscheidet sich vom signalgesteuerten Modell. Wenn der Benutzerthread beim signalgesteuerten Modell das Signal empfängt, bedeutet dies, dass die Daten bereit sind. Anschließend muss der Benutzerthread die E/A-Funktion aufrufen, um den eigentlichen Lesevorgang durchzuführen Schreibvorgänge; im asynchronen E/A-Modell zeigt der Empfang des Signals an, dass der E/A-Vorgang abgeschlossen wurde und es keine Notwendigkeit gibt, die E/A-Funktion im Benutzerthread für tatsächliche Lese- und Schreibvorgänge aufzurufen.
Beachten Sie, dass asynchrone E/A eine zugrunde liegende Unterstützung durch das Betriebssystem erfordert. In Java 7 wird asynchrone E/A bereitgestellt.
Die ersten vier IO-Modelle sind tatsächlich synchrone IOs, und nur das letzte ist wirklich asynchrone IOs, denn unabhängig davon, ob es sich um Multiplex-IOs oder signalgesteuerte Modelle handelt, ist das zweite der IO-Operationen in jeder Phase führt dazu, dass Benutzer-Threads blockiert werden. Das heißt, der Prozess des Datenkopierens durch den Kernel führt zu einer Blockierung von Benutzer-Threads.
6. Zwei Hochleistungs-I/O-Entwurfsmuster
Unter den traditionellen Netzwerkdienst-Entwurfsmustern gibt es zwei klassische Muster:
Eines ist mehrere Threads, eines ist ein Thread-Pool.
Für den Multithread-Modus, das heißt, wenn der Client kommt, erstellt der Server einen neuen Thread, um die Lese- und Schreibereignisse des Clients zu verarbeiten, wie in der folgenden Abbildung dargestellt:
Obwohl dieser Modus einfach und bequem zu handhaben ist, beansprucht er viele Ressourcen, da der Server einen Thread zum Verarbeiten jeder Clientverbindung verwendet. Wenn daher die Anzahl der Verbindungen die Obergrenze erreicht und ein anderer Benutzer eine Verbindung anfordert, führt dies direkt zu einem Ressourcenengpass und in schweren Fällen direkt zum Absturz des Servers.
Um die durch das Ein-Thread-Ein-Client-Modell verursachten Probleme zu lösen, wird daher die Thread-Pool-Methode vorgeschlagen, bei der ein Thread-Pool mit fester Größe und ein Client-Thread erstellt werden Der Pool benötigt einen inaktiven Thread zur Verarbeitung. Wenn der Client die Lese- und Schreibvorgänge abschließt, übergibt er die Belegung des Threads. Dadurch wird die durch die Erstellung von Threads für jeden Client verursachte Ressourcenverschwendung vermieden, sodass Threads wiederverwendet werden können.
Der Thread-Pool hat jedoch auch Nachteile. Wenn es sich bei den meisten Verbindungen um lange Verbindungen handelt, kann es sein, dass alle Threads im Thread-Pool für einen bestimmten Zeitraum belegt sind. Wenn dann ein anderer Benutzer eine Verbindung anfordert , weil es keine gibt Wenn die verfügbaren Leerlauf-Threads für die Verarbeitung verwendet werden, schlägt die Clientverbindung fehl, was sich auf die Benutzererfahrung auswirkt. Daher eignet sich der Thread-Pool besser für eine große Anzahl von Kurzverbindungsanwendungen.
Daher sind die folgenden zwei Hochleistungs-IO-Entwurfsmuster entstanden: Reactor
und Proactor
.
Im Reaktormodus werden die relevanten Ereignisse zuerst für jeden Client registriert, und dann fragt ein Thread jeden Client ab, um zu sehen, ob ein Ereignis auftritt. Die Ereignisse werden von jedem Client nacheinander verarbeitet. Wenn alle Ereignisse verarbeitet sind, werden sie zur Fortsetzung der Abfrage übertragen, wie in der folgenden Abbildung dargestellt:
Wie hier zu sehen ist, gehören zu den fünf oben genannten E/A-Modellen die Multiplex-IO verwendet den Reaktormodus. Beachten Sie, dass die obige Abbildung zeigt, dass jedes Ereignis nacheinander verarbeitet wird, um die Geschwindigkeit der Ereignisverarbeitung zu verbessern. Natürlich können Ereignisse über Multithreads oder Thread-Pools verarbeitet werden.
Wenn im Proactor-Modus ein Ereignis erkannt wird, wird ein neuer asynchroner Vorgang gestartet und dann zur Verarbeitung an den Kernel-Thread übergeben. Wenn der Kernel-Thread den E/A-Vorgang abschließt, wird eine Benachrichtigung gesendet Es ist bekannt, dass das asynchrone E/A-Modell den Proactor-Modus verwendet.
Bitte verzeihen Sie mir, wenn der oben genannte Inhalt Fehler enthält, und freuen Sie sich über Ihre Kritik und Korrekturen!
Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website: JAVA-Video-Tutorial
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung des I/O-Modells in JAVA (mit Beispielen). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!