Java传统IO

传统IO有两种形式,一种是阻塞IO,另一种是阻塞IO+每个请求创建线程/线程池。

阻塞IO

IO的阻塞、非阻塞主要表现在一个IO操作过程中,如果有些操作很慢,比如读操作时需要准备数据,那么当前IO进程是否等待操作完成,还是得知暂时不能操作后先去做别的事情?一直等待下去,什么事也不做直到完成,这就是阻塞。抽空做些别的事情,这是非阻塞。

在传统IO里,InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在网络IO运用中调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回。

 以网络IO操作为例:

 1 public static void main(String[] args) {
 2         Socket socket = null;
 3         try {
 4             ServerSocket ss = new ServerSocket(10000);
 5             while (true) {
 6                 socket = ss.accept();//阻塞
 7                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
 8                 String msg = null;
 9                 while((msg = in.readLine()) != null){//readLine阻塞方法
10                     System.out.println(msg);
11                 }
12             }
13 
14         } catch (Exception e) {
15             e.printStackTrace();
16         } finally{
17           if(socket != null){
18              socket.close();
19            }
20         }
21 }

这种阻塞IO的缺陷如下:

  1. 在调用InputStream.read()/buffer.readLine()方法时是阻塞的,它会一直等到数据到来或缓冲区已满时或超时时才会返回,并且产生了大量String类型垃圾,尽管可以使用StringBuffer优化。
  2. 在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回。
  3. 由于服务器端是单线程的,在第一个连接的客户端阻塞了线程后,第二个客户端必须等待第一个断开后才能连接。
  4. 所有的客户端连接在请求服务端时都会阻塞住,等待前面的完成。即使是使用短连接,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞。这在大规模的访问量或者系统对性能有要求的时候是不能接受的。

阻塞IO + 每个请求创建线程

为每个客户端请求创建单独的线程是对传统的阻塞式IO最常见的解决办法,示例如下:

 1 public static void main(String[] args) {
 2         try {
 3             ServerSocket ss = new ServerSocket(10000);
 4             while (true) {
 5                 final Socket socket = ss.accept();//阻塞
 6                 new Thread(){
 7                     public void run(){
 8                         try{
 9                               BufferedReader in = new BufferedReader(new InputStreamReader(
10                                                   socket.getInputStream()));
11                               String msg = null;
12                               while((msg = in.readLine()) != null){//readLine阻塞方法
13                                      System.out.println(msg);
14                               }
15                         }catch(Exception e){
16                                  e.printStackTrace();
17                        } finally{
18                              if(socket != null){
19                                   socket.close();
20                              }
21                          }
22                     }
23                 }.start();
24             }
25         } catch (Exception e) {
26             e.printStackTrace();
27         }
28 }
这种方式的IO操作模式虽然解决了服务端为单线程的缺陷,但是还是有很多缺陷:
  1. 当客户端较多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间,造成对服务器的压力。服务端的线程个数和客户端并发访问数呈1:1的正比关系,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了
  2. 大量的线程如果需要访问服务端的某些竞争资源,势必需要进行同步操作。每个线程遇到外部未准备好的时候,都会阻塞掉,阻塞的结果就是会带来大量的进程上下文切换。且大部分进程上下文切换可能是无意义的。比如假设一个线程监听某一个端口,一天只会有几次请求进来,但是该 cpu 不得不为该线程不断做上下文切换尝试,大部分的切换以阻塞告终。

阻塞IO + 每个请求被线程池处理

实现很简单,我们只需要将新建线程的地方,交给线程池管理即可。

 1 public static void main(String[] args) {  
 2         try {  
 3             ExecutorService executorService = Executors.newFixedThreadPool(60);  
 4             ServerSocket ss = new ServerSocket(10000);  
 5             while (true) {  
 6                 final Socket socket = ss.accept();//阻塞  
 7                 executorService.execute(new Runnable(){  
 8                      public void run(){  
 9                         try{  
10                               BufferedReader in = new BufferedReader(new InputStreamReader(  
11                                                   socket.getInputStream()));  
12                               String msg = null;  
13                               while((msg = in.readLine()) != null){//readLine阻塞方法  
14                                      System.out.println(msg);  
15                               }  
16                         }catch(Exception e){  
17                             e.printStackTrace();  
18                        } finally{  
19                              if(socket != null){  
20                                   socket.close();  
21                              }  
22                          }  
23                     }  
24                 });  
25             }  
26         } catch (Exception e) {  
27             e.printStackTrace();  
28         }  
29 } 

这种方式实现1个或多个线程处理N个客户端的请求,但是底层还是使用的同步阻塞I/O。

 

 使用FixedThreadPool我们就有效的控制了线程的最大数量,保证了系统有限的资源的控制,但是,正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流进行读取时,会一直阻塞,直到发生:

  1.     有数据可读
  2.     可用数据以及读取完毕
  3.     发生空指针或I/O异常

所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。

posted @ 2021-05-11 17:33  莫待樱开春来踏雪觅芳踪  阅读(78)  评论(0)    收藏  举报