面试-java基础
1.特点:
-1.平台无关性:java编译的字节码文件可以在任意JVM上运行;(跨平台)
-2.面向对象:一切皆对象,使得代码易于维护和重用,包括类,对象,封装,继承,多态,抽象;
-3.内存管理:有自己的垃圾回收机制;
2.JVM,JRE,JDK
-1.JVM是java虚拟机,是java的运行环境,用于将字节码解释或编译为机器码,执行程序;
-2.JRE是java运行时环境包含了JVM和一组java类库,用于支持java程序的运行,不包含开发者工具;
-3.JDK是java开发工具包,包含了JVM,编译器,调试器等开发者工具,还有一系列java类库,提供了全套开发环境;
3.java的解释和编译
-1.Java是编译器和解释器混合的模式,依赖javac,JVM内置解释器和JIT编译器
-2.Java代码经javac静态编译后生成字节码文件,进入JVM
-3.JVM启动初期,解释器会逐行解释字节码,启动快,适合短时间运行的代码,JIT编译器(即时编译器)运行时识别热点代码,那些频繁执行的代码块,将其编译成机器码并缓存进Code Cache,提升执行效率;
当Code Cache被占满时,JIT会停止编译,这时新的热点代码只能通过解释器解释执行
-4.JVM依赖一个方法调用计数器来判段热点代码,当计数器计数大于指定值时,就是判断为热点代码,JIT开始使用
4.值传递和引用传递
-1.java中参数传递只有值传递一种,不存在真正的引用传递;
-2.值传递:传递的是实际值的副本,适用于基本数据类型,修改方法内的参数副本,并不会影响原变量的值;(例子可见小林coding图片)
-3.对于引用数据类型(对象,数组,自定义类等),传递的是对象引用的副本, 原引用和副本引用指向同一个对象,所以通过副本修改对象的内部数据,实际值也会改变,但是如果修改副本的指向,不会影响原引用的指向,不会改变实际值;
5.数据类型:
-1.八种基本的数据类型: byte(一字节->8bit,一个bit表示正负,剩下七个表示大小,-128至127),short(两个字节),int(四个),long(八个),float(四个),double(八个),char(两个),boolean(理论上一个)(具体取值和范围见小林coding图片)
-2.类型向上转型是安全的(例如int->long),向下转型是不安全的,会出现精度丢失,数据溢出等问题; 对象中,父类向下转型为子类,需要使用instanceof来作文能否转型的判断,例如animal instanceof dog;
-3.为什么使用bigDeciaml不用double: double执行的是二进制浮点运算,不能准确的表示一个小数,会出现精度丢失,对于精度要求较高的,要使用bigDecimal,例如bigDecimal num=new bigDecimal(“0.01”), 要运算就是用.add()等方法进行
-4.装箱和拆箱: 自动装箱发生在赋值和方法调用时; 自动装箱的弊端就是会影响性能;
-5.包装类型(Integer等)的必要:int是基本数据类型,而包装类Integer是引用数据类型是object对象; java中大部分方法是用来处理类类型对象的; 例如arraylist集合只能以类作为对象存储;
包装类的应用:1.泛型中 2.转换中:基本类型和引用类型的转换要依赖包装类,例如int转为string要先转为Integer才能转为string 3.集合中:java集合中只能存储对象,不能存储基本数据类型;
-6.对于引用数据类型,对象引用和对象本身是分开存储的,对于基本数据类型,变量对应的内存块直接存储数据本身
-7.Integer的缓存: Java的integer类内部实现了一个静态缓存池,用于存储特定范围的整数值对应的integer对象,-128至127,当创建这个范围内的integer对象实例,并不会生成新的,而是使用缓存中的对象;
6.面向对象:
-1.封装:将对象的属性和行为结合在一起,对外隐藏内部细节,增强安全性,是对象更独立
-2.继承:子类共享父类的数据结构和方法
-3.多态:不同类对象实现同一个接口,每个实例实现不同功能
–1.多态的体现:1.方法的重载(同一个方法名,参数不同) 2.方法重写(子类继承父类,重写父类的方法) 3.接口的实现(实现接口必要重写接口中的方法) 4.向上向下转型
-4.面向对象的六大设计原则:
–1.单一职责原则(SRP):一个类应该只负责一项职责
–2.开放封闭原则(OCP):对扩展开放,对修改关闭
–3.里氏替换原则(LSP):子类必须能够替换父类而不影响程序正确性
–4.接口隔离原则(ISP):客户端不应该依赖它不需要的接口
–5.依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖抽象
–6.最少知识原则(LoD):一个对象应尽量少地了解其他对象
-5.抽象类和普通类的区别:
–1.普通类可以实例化,抽象类不可以,只能被继承
–2.普通类中方法要有具体实现,抽象类可以有实现,也可以没有
–3.一个类只能继承一个普通类或抽象类,都可以同时实现多个接口
-6.抽象类和接口的区别:
–1.抽象类描述类的共同特性和行为,可以有成员变量,构造方法,具体方法;适用于有明显继承关系的场景
–2.接口用于定义行为规范,可以多实现,只能有常量和抽象方法;适用于定义类的能力和功能
-7.抽象类本身不能实例化,但是不意味着不能使用new关键字直接创建一个抽象类的对象; 抽象类可以有构造器,这些构造器在子类实例化时会被调用; 这个过程不是直接实例化抽象类,是创建子类的实例,间接使用类抽象类的构造器;
-8.接口没有构造函数, 构造函数就是初始化class的属性或者方法,在new的一瞬间自动调用,接口都不能new,所以有构造函数也没有意义
7.静态变量和静态方法:
–1.静态变量: 属于类本身,而不是某个对象的变量;
特点:1.共享性:所有对象共享一份 2.初始化:类加载时初始化,只会对其进行一次内存分配 3.访问方式:不依赖对象存在,可以用对象调用但推荐用 类名.变量名 访问
–2.静态方法: 属于类本身的方法,不依赖对象调用
特点: 1.可以不创建对象就能调用 2.只能访问其他静态方法和静态变量,不能直接访问非静态成员 3.静态方法不能重写但是能隐藏
8.静态内部类和非静态内部类的区别:
1.非静态内部类依赖外部类对象存在; 2.静态内部类不依赖外部类对象,本质上等同于一个普通类,只是作用域在外部类内部 3.非静态内部类必须依赖外部类对象创建,隐式持有外部类引用,可以访问外部类的实例和静态成员
4.静态内部类不依赖外部类对象,只能访问外部类的静态成员
9.关键字:
-1.final关键字:用于修饰类,方法和变量
–1.修饰类:当一个类被 final 修饰时,这个类不能被继承;
原因:1.防止被修改行为
如果一个类的功能已经非常稳定,不希望别人通过继承来改变其行为,可以使用 final。
2.保证安全性和一致性
有些底层类如果被继承,可能会引入安全问题或逻辑混乱。
3.便于 JVM 优化
JVM 知道该类不会有子类,在某些场景下可以进行更激进的优化。
–2.修饰方法:当一个方法被 final 修饰时,该方法不能被子类重写(Override);子类可以继承这个方法,但不能修改方法实现
–3.修饰变量:当基本变量被 final 修饰时,它只能被赋值一次,之后不能再改变;当引用变量被final修饰时,只能修改变量内部属性值,但是不能修改引用变量指向的对象
-2.static关键字:static 表示 属于类,而不属于对象;static 的东西在类加载时就存在,而非 static 的东西必须等对象创建之后才存在
–1.修饰变量:当一个变量被 static 修饰时,这个变量:属于类,被所有对象共享,在内存中只有一份,无论创建多少个对象,访问的都是同一个变量
–2.修饰方法:当一个方法被 static 修饰时:方法属于类,不依赖对象存在,可以通过 类名.方法名 直接调用; 不能使用this,super;不能访问非静态成员
–3.修饰代码块:修饰的代码在类加载时执行,且只执行一次,比构造方法早;
–4.修饰内部类: 不依赖于外部类,可以独立存在,可以直接创建;
10.深拷贝与浅拷贝:
-1.浅拷贝:浅拷贝在复制对象时,只复制对象本身这一层的内容。对于基本数据类型,会直接复制值;对于引用类型,只会复制引用地址,而不会复制引用指向的实际对象。
因此,浅拷贝后产生的新对象和原对象在引用类型成员上是共享同一块内存的。如果通过其中一个对象修改了引用对象的内容,另一个对象中看到的内容也会随之改变。
-2.深拷贝:深拷贝在复制对象时,不仅复制对象本身,还会把对象中所有引用的对象也一并复制,重新创建新的实例。整个对象结构都会被完整地拷贝一份。
因此,深拷贝后,新对象与原对象在内存中是完全独立的。无论如何修改其中一个对象及其内部数据,都不会对另一个对象产生任何影响。
-3.实现深拷贝的三种方式:<具体代码那看小林coding>
–1.实现cloneable接口并重写clone()方法名
–2.使用序列化和反序列化
–3.手动递归复制
11.泛型:
在编译期约束类型、提高代码复用性和安全性。通过在类、接口或方法中引入类型参数,可以在使用时明确指定具体类型,避免把错误类型的数据传进去,从而把原本运行期可能出现的类型错误提前到编译期发现;
12.对象:
-1.对象的本质与组成:Java 对象是根据类创建出来的实例,类是对象的模板,对象是类的具体化。一个对象在逻辑上由两部分组成:属性(成员变量)和行为(成员方法)。属性用来描述对象的状态,方法用来描述对象能做什么。
在 JVM 层面,一个对象在内存中还包含对象头(Object Header)、实例数据以及对齐填充。对象头中保存了运行时元数据,如哈希码、GC 信息、锁状态以及指向类元数据的指针
-2.创建方式<具体代码看~>: 1.new关键字 2.通过反射机制使用Constructor的newInstance()方法创建 3.使用clone()方法 4.使用反序列化 5.使用工厂模式
-3.new出的对象的回收: 由GC根据一定算法来回收
-4.私有对象: 被private修饰,只有所在类内部被访问; 间接获取方法:1.相应的getter方法 2.反射机制
13.反射:
反射(Reflection)是一种在运行期动态获取类信息并操作类和对象的机制
JVM 在类加载完成后,会为每个类生成一个对应的 Class 对象,这个 Class 对象中保存了该类的所有结构信息(构造器、方法、字段、注解等)。反射本质上就是通过 Class 对象来操作类的元数据
反射允许在运行时再决定“操作哪个类、哪个方法、哪个字段”
特性:
-1.运行时类信息访问
-2.动态对象创建: 可以使用Class类或Constructor对象的newInstance()方法创建
-3.动态方法调用: Method类的invoke()方法实现
-4.访问和修改字段: 通过Field类的get(),set()方法
应用场景:
-1.加载数据库驱动:Class.forName(“com.mysql.cj.jdbc.Driver”); 通过不同的参数来指定不同数据库,如mysql,Oracle;
-2.配置文件加载: spring框架的IOC容器动态管理bean
14.注解:
注解的本质是继承了Annotation的特殊接口,其具体实现类是java运行时生成的动态代理类; 读取注解的程序(编译器、JVM、框架)
Java 注解本质是一种元数据,编译后存储在 class 文件中,运行期通过反射读取。
JVM 使用动态代理为注解生成实例,框架通过解析注解配置来驱动程序行为,注解本身不包含任何业务逻辑
底层实现:
-1.注解的作用范围:
–1.源码级别注解:仅存在源码中,编译后不保留@Retention(RetentionPolicy.SOURCE)
–2.类文件级别注解:保留在.class文件中,但运行时不可见(默认)@Retention(RetentionPolicy.CLASS)
–3.运行时注解:保留在.class文件中,但运行期可反射获取 @Retention(RetentionPolicy.RUNTIME) 只有 RUNTIME 才能被框架使用(spring,springboot等)
-2.当注解被标记为RUNTIME时,java编译器会在.class文件中保存注解信息; 这些信息存贮在字节码的属性表中(Attribute Table)
包括:
–1.RuntimeVisibleAnnotations 存储运行时可见的注解信息
–2.RuntimeInvisibleAnnotations 存储运行时不可见的注解信息
–3.RuntimeVisibleParameterAnnotations和RuntimeinvisibleParameterAnnotations 存储方法参数上的注解信息
-3.注解的解析: 主要依赖于java的反射机制和字节码文件的存储
解析注解流程:
–1.获取注册信息: 通过反射API获取类,方法等元素上的注解
–2.底层原理:反射机制的核心类AnnotatedElement是所有可以被注解修饰的元素(类,方法等)的父接口; JVM在加载类时会解析.class文件中的注解信息,并将其存储在内存中,供反射机制使用;
-4.java注解的作用域:
–1.类级别作用域: 注解作用在类、接口、枚举或注解类型本身上,通常用于描述一个类的整体特性或角色。
常见用途包括标记某个类的功能定位、生命周期、是否交由框架管理等,例如 @Controller、@Service、@Entity。
类级别注解一般在框架启动或类加载阶段被扫描,用于决定该类是否需要被实例化或增强处理。
–2.方法级别作用域:注解作用在方法上,用于描述方法的行为、权限、事务特性或调用规则。
常见用途包括事务控制、接口映射、权限校验等,例如 @Transactional、@RequestMapping、@Override。
方法级别注解通常在方法调用前或调用过程中通过反射或代理机制生效。
–3.字段级别作用域:注解作用在成员变量上,用于描述字段的注入方式、映射关系或校验规则。
常见用途包括依赖注入、ORM 映射、参数校验等,例如 @Autowired、@Value、@Column。
字段级别注解一般在对象创建或属性赋值阶段被框架解析并处理。
–4.其他注解作用域:构造函数作用域,局部变量作用域等
构造函数作用域用于描述对象创建过程中的行为或依赖注入规则,例如构造器注入相关注解。
参数作用域用于描述方法参数的来源、校验规则或绑定关系,例如 @RequestParam、@PathVariable。
局部变量作用域主要用于编译期检查或工具分析,对运行期逻辑影响较小,例如 @SuppressWarnings
15.异常:
异常类层次结构图具体看<小林coding>
-1.java异常体系主要基于Throwable及其子类; Throwable有两个重要的子类:Error和Exception,代表不同的类型异常;
-1.Error:表示程序无法处理的严重问题,通常是由 Java 虚拟机自身或底层系统引发的。这类错误往往意味着系统处于不正常、不可恢复的状态,比如内存耗尽、栈溢出、类加载失败等。
Error 一般不是代码逻辑导致的,而是运行环境或 JVM 层面的问题。由于其严重性,程序通常不应该也无法通过捕获 Error 来进行恢复,常见的做法是让程序直接终止。
-2.Exception:表示程序在运行过程中可以预期、可以处理的异常情况,通常与业务逻辑或外部条件有关,比如文件不存在、网络连接失败、参数非法等。Exception 的设计目的就是让程序有机会通过捕获并处理异常,
从而继续运行或给出合理的提示。Exception 又可以细分为运行时异常(RuntimeException 及其子类)和受检异常(非 RuntimeException),前者通常由程序逻辑错误引起,后者则需要在编译期显式处理或声明抛出。
-3.异常的处理:
–1.try-catch语句块,在catch中执行处理逻辑
–2.throw语句,手动抛出异常: throw new Exception(“message”);
–3.throws关键字:在方法或类上添加throws关键字,声明可能抛出的异常
–4.finally块: 无论是否抛出都会执行的代码
-4.try-catch语句运行情况:try中的代码按顺序进行,如果抛出异常,则在catch中匹配并执行catch中的语句;如果没有匹配的catch块,异常将被传递给上一层调用的方法;
-5.如果try和finally中都有return语句,最终执行的是finally中的;
16.Object类:
Object类是所有类的超类,默认提供11个核心方法,核心用于对象比较,哈希,字符串表示,线程同步等;
-1.equals()方法: 默认比较的是两个对象的地址;如果有其他要求要重写方法;
-2.hashcode()方法: 重写了equals方法一定要重写hashcode()方法,因为如果两个对象equals返回true那么hashcode一定相等,如果hashcode不相等那么equals一定返回false;
-3.toString()方法: 返回 类名@十六进制哈希码
-4.getClass()方法: 这个方法不能重写,返回的是运行时实际的类对象; 例如 animal a=new dog(); 返回的是dog的类对象;
-5.clone()方法: 用于创建对象的浅拷贝,使用时要让类实现Cloneable接口;
-6.notify和nitifyall方法: 都用于多线程同步,和synchronized配合使用; notify() 用于唤醒一个正在等待该对象监视器(锁) 的线程;
wait方法有三个重载,作用是让当前线程释放锁并进入等待状态,直到被notify和nitifyall唤醒或等待时间到期;同样需要在synchronized同步块或方法中使用;
notify() 只是通知等待线程可以准备运行了,但不释放当前线程持有的锁;
被唤醒的线程会从 wait() 的阻塞状态转为就绪状态,但必须等待当前线程释放锁。
-7.: finalize方法: 用于垃圾回收,现在已经不推荐用;
17.==和equals的区别:
==比较的是地址,equals比较的是内容;
18.hashCode()和equals()的关系:
为了保证在哈希集合(HashSet、HashMap等)中正确工作。如果两个对象相等但哈希码不同,它们会被放到哈希表的不同位置,导致集合出现重复元素或查找失败;所以重写 equals() 必须重写 hashCode()否则会产生哈希冲突;
19.string的常用方法:
-1.length()方法:
-2.equals()方法:判断内容
-3.substring()方法: 截取子串
-4.trim()方法: 去除字符串首尾的空白字符
-5.replace()方法: 替换所有指定字符 replace(char oldChar,char newChar)
-6.isEmpty()方法: 判空
20.String,StringBuffer,StringBuilder的区别和联系<具体对比看小林>:
-1.可变性:string不可变, stringBuilder和stringBuffer可变,改变内容并不会创建新的字符串对象;
-2.线程安全性:string不可变,天然线程安全; stringbuilder不是线程安全的,适用于单线程环境;stringbuffer线程安全,其方法通过synchronized关键字实现同步,适用于多线程环境;
-3.性能: string性能最低,stringbuilder性能最高,因为没有线程安全的开销;stringbuffer次之,因为他的线程安全机制引入了同步开销
-4.使用场景: 固定不变用string,需要频繁修改的单线程用stringbuilder,需要频繁修改的多线程用stringbuffer
21.java8新特性:
-1.lambda表达式:
-2.函数式接口: 只有一个抽象方法的接口
-3.StreamAPI:
-4.Optional类:
-5.方法引用: 简化Lambda表达式,直接引用现有方法 Integer::intValue 等价于 x->int
-6.接口的默认方法和静态方法:
-7.并行数组排序:使用多线程加速数组排序: Arrays.parallelSort(array);
-8.重复注解:
-9.类型注解:
-10.CompletableFuture:
22.lambda表达式:
用于创建匿名函数的简洁语法.主要用于函数式接口; 函数体中包含多个语句时:(参数)->{语句}; 只包含一个语句时: (参数)->语句;
21.stream的API:
提供链式操作处理集合,支持并行处理: list.stream().filter(x->x>0).collect(Collectors.toList());
22.stream流的并行API:
是ParallelStream; 就是将一个数据源分为多个子流对象进行多线程操作,最后再将处理结果汇总为一个流对象;底层使用的是fork/join池来实现; 适合cpu密集型的任务,不适合io密集型任务
23.CompletableFuture用法:
CompletableFuture 是 JDK 8 引入的一个异步编程工具类; 相比future解决的问题:非阻塞,支持回调,支持任务链,支持多个任务并行/串行组合,支持优雅的异常处理;
并行调用多个服务的场景:
1 | CompletableFuture<User> userFuture = |
24.java21新特性:
-1.新语言特性:
–1.switch语句的模式匹配: 允许再switch case中使用模式匹配;
以前的 switch
1 | switch (obj) { |
Java 21 的 switch(更强)
1 | switch (obj) { |
–2.数组模式: 将模式匹配括展到数组中;
–3.字符串模板: 支持在字符串字面量中直接嵌入表达式; 原本的的”hello”+name; 可变为: hello{name};
-2.新并发特性:
–1.虚拟线程(Virtual Threads): 轻量级并发的新选择,通过共享堆栈的方式大大降低了内存消耗,提高了吞吐量(单位时间内,系统能处理的任务数量)和响应速度; 可以通过静态构建方法,构建器或ExecutorService来创建使用;
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();executor.submit(() -> {
System.out.println("task");});
–2.Scoped Value(范围值): 提供了一种在线程间共享不可变数据的新方式;
1 | static final ScopedValue<String> USER = ScopedValue.newInstance(); |
25.序列化:
-1.将对象从一个jvm转移到另一个jvm的方式:
–1.使用序列化和反序列化: 将对象转换为可传输的字节流(如通过 Java Serializable、JSON、Protobuf 等方式),通过网络或文件传输到另一个 JVM,在目标 JVM 中再反序列化还原为对象。该方式实现简单,但需要对象支持序列化,且存在性能开销
–2.使用消息传递机制: 将对象的数据封装为消息,通过消息中间件(如 Kafka、RabbitMQ、RocketMQ 等)在不同 JVM 之间传递。接收方 JVM 根据消息内容重新构建对象,适用于解耦系统和异步通信场景
–3.使用远程方法调用: 通过远程方法调用的方式,由一个 JVM 调用另一个 JVM 中的对象方法,底层由 RPC 框架完成参数对象的序列化、网络传输和反序列化。常见实现包括 Java RMI、gRPC、Dubbo 等
–4.使用共享数据库或缓存: 将对象的数据持久化到共享存储(如数据库、Redis 等),另一个 JVM 从该存储中读取数据并重建对象。该方式适合数据共享和状态同步,但实时性较低,且存在一致性问题
-2.序列化,反序列化的实现: 序列化(Serialization) 是指将 Java 对象转换为可存储或可传输的字节序列的过程;
反序列化(Deserialization) 是指将字节序列重新还原为 Java 对象的过程。
在 Java 中,对象要实现序列化通常需要实现 Serializable 接口(或 Externalizable 接口)。
序列化过程中,JVM 会将对象的类信息、字段值等写入字节流;反序列化时,JVM 根据字节流中的数据重新创建对象实例。
-3.将对象转为二进制字节流具体怎么实现: 在 Java 中,通常通过 对象输出流和对象输入流 来实现对象与二进制字节流之间的转换。
序列化实现步骤:
1.对象所属的类实现 Serializable 接口;
2.使用 ObjectOutputStream 将对象写入输出流,通过writeObject()方法做序列化操作;
3.JVM 将对象的状态转换为二进制字节流并输出。
反序列化实现步骤:
1.使用 ObjectInputStream 读取二进制字节流,通过readObject()方法做反序列化操作;
2.JVM 根据字节流信息创建对象实例;
3.恢复对象的字段值,完成反序列化。
26.设计模式:
-1.单例模式: volatile关键字和sychronized实现单例模式;<具体看小林>; volatile作用:1.保证可见性 ;2.禁止指令重排优化;
-2.代理模式和适配器模式的区别:
–1.目的不同:代理模式主要目的是控制对目标对象的访问;适配器模式主要目的是解决接口不兼容的问题。
–2.结构不同:
代理模式结构:
Client → Proxy → RealSubject
Proxy 和 RealSubject 实现同一接口
适配器模式结构:
Client → Adapter → Adaptee
Adapter 实现客户端接口
Adaptee 是已有的、不兼容的类
–3.应用场景不同:
代理模式常见场景:
远程代理(RPC、RMI)
虚拟代理(延迟加载大对象)
权限控制代理
AOP(Spring 动态代理)
适配器模式常见场景:
使用第三方库但接口不匹配
新系统兼容老系统接口
不修改旧代码却要复用其功能
-3.责任链模式: 是一种行为型设计模式; 将请求的发送者与接收者解耦,让多个处理对象都有机会处理该请求,请求沿着一条“链”依次传递,直到被某个对象处理或链结束。 <具体代码那看小林coding,在接近最后部分>
-4.策略模式: 将一组可互换的算法封装成独立策略,根据条件选择其中一个执行
-5.策略模式和责任链模式应用场景:
–1.策略模式:当同一业务有多种实现方式或算法,但一次只需要选择其中一种来执行时使用策略模式。它通过把不同算法封装成独立策略,根据条件在运行时进行选择,
避免大量 if-else 或 switch,常用于价格计算、支付方式选择、算法切换、规则差异化处理等场景,核心关注点是**“怎么做”。
–2.责任链模式:当一个请求可能需要经过多个处理者按顺序处理,且具体由谁处理在运行时决定时使用责任链模式。它将多个处理器串成一条链,请求在链中逐级传递,
直到被处理或结束,常用于审批流程、参数校验、过滤器/拦截器、权限校验、日志和异常处理等场景,核心关注点是“谁来处理”**
27.IO:
-1.java怎么实现IO高并发编程:Java 主要通过 非阻塞 IO 与多路复用机制 来支撑高并发场景。传统的 BIO 采用“一连接一线程”模型,在线程数量和上下文切换上存在瓶颈;
为此,Java 提供了 NIO,通过 Channel + Buffer + Selector 实现 非阻塞 IO 和 IO 多路复用,一个线程即可监听并处理大量连接的读写事件;
在此基础上常结合 Reactor 模型(接收连接与读写处理解耦)和 线程池 将耗时业务异步化。进一步,Java 还提供 AIO(Asynchronous IO),基于回调或 Future 实现真正的异步读写,减少线程阻塞;
实际工程中(如 Netty),通常在 NIO 之上封装高效的事件驱动模型、零拷贝、内存池和高性能线程模型,从而实现高吞吐、低延迟的 IO 高并发处理;
-2.BIO,NIO.AIO的分别介绍与区别:
–1.BIO:BIO 是传统的同步阻塞 IO 模型,读写操作会阻塞线程,通常采用“一连接一线程”的处理方式。实现简单、逻辑直观,适合连接数少、并发量低的场景,但在高并发下会因线程数量过多、频繁上下文切换而导致性能和资源消耗问题
–2.NIO:NIO 是同步非阻塞 IO,通过 Channel + Buffer + Selector 实现,一个线程可以同时监听多个连接的 IO 事件,只有在数据就绪时才进行读写操作。它基于 IO 多路复用机制,显著减少线程数量,适合高并发、高吞吐场景,是目前 Java 网络编程的主流方案(如 Netty)
–3.AIO:AIO 是异步非阻塞 IO,读写操作由操作系统异步完成,应用程序通过回调或 Future 获取结果,线程在发起 IO 后无需等待。它可以进一步降低线程阻塞,提高并发能力,但编程模型相对复杂,对操作系统支持依赖较强,实际使用场景相对较少。
–4.区别:BIO 是同步阻塞,实现最简单但扩展性最差;NIO 是同步非阻塞,通过多路复用提升并发能力,是工程中最常用的方案;AIO 是异步非阻塞,理论并发能力最高,但复杂度和环境依赖也最高。总体来看,低并发用 BIO,高并发首选 NIO,追求极致异步且平台支持良好时可考虑 AIO
-3.NIO如何实现:Java NIO 通过 非阻塞 IO + IO 多路复用 来实现高并发处理,其核心在于 Channel、Buffer、Selector 三个组件协同工作。
服务端首先将 ServerSocketChannel 和 SocketChannel 设置为非阻塞模式,并注册到 Selector 上,关注连接、读、写等事件;
Selector 内部基于操作系统的多路复用机制(如 Linux 的 epoll),通过一次 select() 调用监听多个 Channel 的就绪状态;
当某个 Channel 就绪时,线程被唤醒并根据事件类型进行读写操作,数据通过 Buffer 在用户空间和内核之间传输。
整个过程中,一个或少量线程即可管理成千上万的连接,通常再结合 Reactor 模型 和 线程池 将耗时业务处理与 IO 读写解耦,从而实现高吞吐、低资源消耗的 NIO 高并发服务器
-4.Netty: Netty 是一个基于 Java NIO 的高性能、异步、事件驱动的网络通信框架,对底层复杂的 NIO、Selector、多线程模型、TCP 细节进行了高度封装,让开发者可以用简单、可靠的方式写出高并发网络应用;
采用epoll模式后,只需要一个线程负责selector的轮询;当有数据就绪后需要一个事件分发器,负责将读写事件分发给对应的读写处理器;时间分发器有两种模式:Reactor和Proactor; Reactor采用同步IO,Proactor采用异步IO