01BIO-同步并阻塞IO的讲解

本文介绍了Java中的BIO( Blocking IO)模型,详细解析了其同步并阻塞的特点,并通过三个案例探讨了BIO在实际应用中的问题和优化策略。案例包括简单的TCP通讯、使用线程池优化多线程通讯以及BIO模式下的文件上传。通过线程池和阻塞队列控制并发,减少了资源浪费,但BIO模式仍存在效率问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

BIO(Blocking IO):同步并阻塞IO,最为传统的通讯模式,意味着一个连接就有一个线程,比如服务器启动好了,我们3个人要去连接服务器,我们就是客户端,我们每连接进去一个人,服务端就会单独给我们启动一个线程,这样有坏处,就是如果我们连接进去啥也不做,线程一直在那边挂着,会造成一种资源浪费的情况,虽然后续有一些基于现模式的处理方式,但是还有有一些不可忽略的缺陷。

案例

案例一:简单的通讯方式(基于TCP连接)

代码略…
代码省略的原因是这个很简单,不管你是学校,还是培训机构学的,还是网上自学,最先接触线程老师都会给你这样一个demo,这个demo实现起来很简单,带大家复习一下,提供思路。

首先这种模式基于一个服务端,一个客户端,通过socket交流,客户端连接服务端的IP,端口,服务端一直监听和客户端一样的端口号,然后客户端通过输出流向服务端输出信息,服务端当然就是输入流,来接受客户端的信息,当然两边都可以用输入/输出流来交流。如果是需要多个线程交流。那么就不用直接在服务端main方法里面处理,写一个线程类,把socket通过构造器传入线程,剩余的操作和服务端操作一样,服务端只需要通过serverSocket.accept()去获取客户端的socket传入线程,然后服务端死循环去接受客户端的socket,在循环里面创建线程,每来一个客户端创建一个线程,在循环里面启动线程就可以多个线程间通讯了,但是这种方式会造成很大程度的资源浪费,特别是人多的时候。后续有处理方式,但是还是没从根本上解决问题。

这种方式的缺陷:

  1. 每进入一个客户端都会创建一个线程,线程竞争影响性能
  2. 我们知道每个线程都会创建一个栈,线程过多会导致CPU资源被占用过多,造成性能低下
  3. 严重的情况,特别是并发的情况下,大量客户端突然访问,你说这不造成栈内存溢出么?

当然如果你说,你就是在你们公司内部,学校内网或者某个政府部门里面进行通讯,这样实际上并发不大,用户不多,经过改良也是可行的。

案例二:利用线程池来优化多线程通讯(伪异步IO)

介绍:利用线程池和任务队列来实现,由于我们可以给线程池设置一些参数,那么对于资源的利用不会像之前一样无节制,是认为可控的,那么这样就可以在一定程度下解决和控制线程的并发。

// 客户端
public class Client {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1",7777);
        OutputStream os = socket.getOutputStream();
        PrintStream ps = new PrintStream(os);

        Scanner sc = new Scanner(System.in);

        while (true){
            System.out.println("请输入信息:");
            String msg = sc.nextLine();

            ps.println(msg);
            ps.flush();
            System.out.println("已送达:"+msg);
        }

    }
}

上面是客户端,这个不多BB

/**
 * 服务端监听客户端socket的线程类
 */
public class ServerSocketThread implements Runnable{
    private Socket socket;

    public ServerSocketThread(Socket socket){
        this.socket =socket;
    }

    @Override
    public void run() {

        try {
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String str = "";
            while ((str=br.readLine())!=null){
                System.out.println("接受信息:"+str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

上面这个是服务端监听客户端的线程类,也简单,不多bb,上面说过,要想多个线程来访问服务端,那么我们得循环接受客户端socket,服务端去监听它,每监听到一个,代表一个上线,每个人每个线程。

public class HandlerSocketServerPool {
    private ExecutorService executorService;
    
    public HandlerSocketServerPool(int maxPoolNum,int queueSize){
        //核心线程数,最大线程数,线程最大空闲时间,单位,线程等待队列
        executorService =
                new ThreadPoolExecutor(3,maxPoolNum,3,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
    }

    public void execute(Runnable target){
        executorService.execute(target);
    }

}

Socket线程池,待会介绍参数

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(7777);

        HandlerSocketServerPool pool = new HandlerSocketServerPool(4,3);
        while (true){
            Socket s = ss.accept();
            Runnable target = new ServerSocketThread(s);
            pool.execute(target);
        }
    }
}

上面是服务端,用池得概念把客户端socket存起来,执行得时候传入线程,会自动去调用当前线程的run方法。
下面说一下这个线程池,以及一些参数的含义。

ExecutorService

源码方面不多分析,我也没仔细看,大家有空可以去看,只做应用方面的介绍
在这里插入图片描述
这个是线程池接口,我们代码里面用的实现类是ThreadPoolExecutor,看它的构造方法

public ThreadPoolExecutor(
			//核心线程数,意味着最少的线程数量,
			//如果没有人连接也会有2个闲置线程
			int corePoolSize, 
			//最大线程数,意味着我们最大的可用线程数
			//如果连接超过这个数,不好意思,你只有去后面排队
			//等着其他人离开了你再进去
            int maximumPoolSize,
            //线程最大空闲时间
            long keepAliveTime,
            //前一个参数的单位
            TimeUnit unit,
            //线程等待队列
            BlockingQueue<Runnable> workQueue
 ) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

BlockingQueue这个是一个阻塞式消息队列,其实就是一个生产者消费者模式,
点击参考我之前写的简单的生产者消费者模式讲解

点击参考别人对BlockingQueue的讲解

生活化举例

担心线程池的参数可能让大家懵逼,我们模拟一个场景举例子:

  • xx银行今天3个柜台可以办理业务(核心线程==3)
  • 如果来的人大于3个,比如来了5个,多的2个去休息区排队,休息区最多容纳2个人,只能等前面办业务的3个人办完了,才能去,先排队的先去(阻塞队列BlockingQueue的数量==2)
  • 核心线程3+BlockingQueue的数量2=5,也就是柜台+等待区的数量5,也就是说我们最多容纳5个人,但是如果进来第六个咋办?如果我们设置了最大线程数5,那么此是柜台数量由3变成5,那么消息队列排队的2个人会去4,5号柜台办理业务,新来的人会进入消息队列等待。
  • 此时柜台数量5,等待区2,最多容纳7个,如果来8个人,第八个人直接会被拒绝,服务端会报错。
    通过上述例子,我们可以得知
  • 最大线程数必须大于核心线程数
  • 如果我们上面代码启动的客户端>核心线程数,后续的(未超过最大线程数的客户端),进入消息队列等待,只有核心线程数的客户端可以对话。
  • 如果启动的客户端>核心线程数+消息等待队列的数量,那么可以理解当前最大可用线程的数量(可以直接对话的)为最大线程数,最大容纳线程数为最大线程数+消息队列数量,超过则报错
案例三:BIO模式客户端向服务端上传文件

稍微说一下,直接贴代码,其实就是客户端用字节输出流或者数据输出流向服务端发送二进制的数据,发送完后利用socket.shutdownOutput()通知服务端自己传送完了,别接受数据了(也可以直接关闭socket的输出流),
服务端当然是用同样对于的输入流去读取数据,实现文件上传功能,也可以说拷贝,我们这里用的线程,和上面差不多,目的是保证客户端能持续向服务端上传文件,不至于说是传完就结束了,服务端死循环,一直等人给它传数据。

客户端

public class Client {
    public static void main(String[] args) {
        try(
                InputStream fis = new FileInputStream("E:\\many_codes\\demos\\BIO-NIO-AIO\\src\\main\\resources\\orign\\奥力给.jpg");
           ) {
            Socket socket = new Socket("127.0.0.1",2005);

            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            dos.writeUTF(".jpg");
            byte[] buf = new byte[1024];
            int len;

            while ((len=fis.read(buf))!=-1){
                System.out.println(len);
                dos.write(buf,0,len);
            }
            dos.flush();
            // 输出流输出完毕后
            //DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //里面的socket.getOutputStream()关闭
            socket.shutdownOutput();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端

public class Server {
    public static void main(String[] args) {
        System.out.println("服务端启动...");
        try {
            ServerSocket ss = new ServerSocket(2005);
            while (true){
                Socket socket = ss.accept();
                new ServerThread(socket).start();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

监听socket的线程

public class ServerThread extends Thread{
    private Socket socket;

    public ServerThread(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        OutputStream os = null;
        try {
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            String suffix = dis.readUTF();
            System.out.println("接受文件类型:"+suffix);
            os = new FileOutputStream("E:\\many_codes\\demos\\BIO-NIO-AIO\\src\\main\\resources\\target\\"+ UUID.randomUUID()+suffix);
            byte[] buff = new byte[1024];
            int len ;

            while ((len = dis.read(buff))!=-1){
                os.write(buff,0,len);
            }

            System.out.println("文件接受成功");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (os!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

好的,BIO第一节的内容差不多结束了,可能写得不是很好,如果里面出现知识性的错误,希望大家及时私信或者留言提示我,我一定在确定对错后迅速纠正,从而避免误导其他学习的小伙伴(这个很重要,要让正确的知识传播,而不是错误的)
原创不易,觉得不错的话,给个赞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值