Java基础概念
java语言有哪些特点
- 面向对象: 封装,继承,多态
- 平台无关: 虚拟机机制,使得Java 语言在不同平台上运行不需要重新编译
- 安全性: Java提供一些列安全防护措施和访问修饰符,限制直接访问操作系统
- 可靠性: 异常处理与内存管理
- 高效性: 执行效率还行
- 解释和编译并存: Java先将程序编译为字节码,然后交给JVM解释器来运行
JVM、JDK、JRE的关系
JDK是功能齐全的Java SDK,它包括JRE,以及javac(java编译器),还有一些常用的工具:javadoc(文档注释工具),jdp(调试工具),jconcole(可视化监控工具),javap(反编译工具)
JRE是Java运行环境,它包含两个部分:JVM和Java 核心类库
JVM是运行Java字节码的虚拟机,针对不同的操作系统有不同的实现,目的是相同的字节码文件能产生相同的结果。JVM不止一种,只要满足规范,任何人都可以开发自己的JVM
什么是字节码?采用字节码的好处
在java中,字节码(.class文件,十六进制组成)是指能够被JVM理解的代码。
它不面向任何特定的机器,只面向虚拟机,因此将Java编译成字节码文件后,在任何操作系统上都可以运行。这也使得Java具有优异的跨平台移植性以及不错的执行效率。
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言(Java 通过引入JIT,可以理解为编译型语言,但整体上通过JVM解释运行)执行效率低的问题,同时又保留了解释型语言可移植的特点。
JIT和AOT
在JVM中,需要解释器逐行将字节码翻译成机器码执行程序,这样的效率不高。
因此引入了JIT,它属于即时编译,他采用惰性评估,在运行时找到热点代码,并编译成机器码后进行保存,从而提升了java运行效率。
AOT是直接将所有字节码编译成机器码,这种方式效率很高,但是与java语言的动态特性不太兼容,动态特性例如:在运行时生成并加载修改后的字节码
JAVA和C++的区别
- java不提供指针来直接访问内存
- C++ 支持多继承,而 Java 不支持多重继承,但允许一个类实现多个接口
- java有自动内存管理垃圾回收机制,无法手动释放内存
- java只能重载方法,c++还可以重载操作符
基础语法
基本类型和包装类型的区别
- 用途:基本数据类型一般用于常量和局部变量,而包装类一般用于方法参数,并且可以使用泛型
- 存储位置:基本数据类型存于栈中,包装类(非static)存储在堆中
- 大小:基本数据类型占用空间很小,而对象会相对较大
- 比较方式:基本数据类型可以直接用==比较,而包装类之间比较用equals
- 默认值:包装类的默认值为null
包装类型的缓存机制
Byte、Short、Integer、Long缓存了[-128, 127]的值,Character缓存了[0, 127]的值,Boolean缓存了true和false
因此当自动装箱(valueOf方法)时,如果是缓存中的值则直接访问缓存对象,因此出现以下现象
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i1 = 255;
Integer i2 = 255;
System.out.println(i1 == i2);// 输出 fasle
Integer i1 = 40;
Integer i2 = new Integer(40); // 创建了新对象
System.out.println(i1==i2); // 输出false
静态方法为什么不能调用非静态成员
因为静态方法在类加载时就同时加载了,而非静态成员是在类实例化的时候才加载。相当于静态方法加载的时候,非静态成员还不存在,因此无法调用
重载和重写的区别
重载:指在同一个类中,方法名相同,但是参数列表(个数,类型,顺序)、返回值、修饰符、异常可以不同;重载用于让同一个方法对不同的参数列表实现不同的逻辑
重写:发生在子类中,子类继承父类的方法可以将其重写,重写的方法名必须相同,参数列表必须相同,返回值、修饰符以及异常要小于父类;重写用于让子类改造父类方法的内部逻辑
接口和抽象类的区别
- 一个类可以实现多个接口,但只能继承一个抽象类
- 接口中没有构造方法和普通成员变量,抽象类中可以有构造方法和成员变量
- 接口和抽象类都可以有静态成员变量,但接口默认且只能public final修饰,抽象类不限制
- 抽象类可以有普通方法;接口只能有抽象方法,在JDK8可以有默认方法,在JDK9后可以有私有方法
- 抽象类可以有静态方法;接口没有,在JDK8可以有,且只能被接口类所调用
访问修饰符public、private、protected、以及默认的区别
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
面向对象
面向对象和面向过程的区别
两者的区别在于解决问题的方式不同:
**面向过程:**将解决问题的过程拆分成一个个方法进行解决
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展。
**面向对象:**会抽象出一个对象,通过对象的方法来解决问题;同时具有易维护、易复用、易拓展的优点
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
- 缺点:性能比面向过程低。
面向对象三大特征
- 封装:对象可以隐藏自己的属性和方法不被外界访问
- 继承:通过继承子类可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
- 多态性:它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
构造方法有哪些特点
- 方法名与类名一致
- 没有返回值,不能用void修饰
- 创建对象时自动调用
- 不可重写,但可以重载
对象相等判断
== 和equals 的区别
== 常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较;
- 如果
==比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等; - 如果
==是比较的两个对象,那么比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域;
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象
- 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
- 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容
介绍下hashCode()
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置(哈希码对数组的长度取余后会确定一个存储的下标位置)。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
为什么要有hashCode?
以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,
如果没有相符的hashcode,HashSet会假设对象没有重复出现。
如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
引用拷贝、深拷贝和浅拷贝区别?
**引用拷贝:**直接复制对象的引用地址
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
String 相关
String、StringBuffer和StringBuilder的区别
-
可变与不可变:
-
String类中使用字符数组保存字符串,因为有**
final**修饰符,所以string对象是不可变的。对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.
-
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。
-
-
是否线程安全:
-
String中对象是不可变的,可以理解为常量,因此线程安全
-
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
-
StringBuilder是非线程安全的
-
-
性能
-
每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象,因此性能较差
-
StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 的性能提升,但却要冒多线程不安全的风险。
-
String为什么要设计成不可变的?
-
便于实现字符串池(String pool)
-
使多线程安全
-
加快字符串处理速度:由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
什么是字符串常量池?
**字符串常量池:**jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
在jdk8中,常量池的位置再元空间(永久代(方法区)被元空间取代)
Java中三个常量池概念:
- **
全局字符常量池**在每个JVM中只有一份,存放的是字符串常量的引用值。 - **
class常量池**是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。 - **
运行时常量池**是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
String str="aaa"与 String str=new String("aaa")一样吗
- 使用
String a = “aaa” ;,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用
String b = new String("aaa");,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
String.intern()
将字符串保存到常量池:如果有了就返回引用;没有就创建并返回
字符串+
“+”和“+=”在字符串操作时,java是对这两个操作符重载了,在底层,会创建StringBuilder对象,使用append方法拼接,然后返回toStirng方法产生的对象
但是在循环场景下会大量地创建StringBuilder对象,导致性能也不高,最好还是自己使用StringBuilder(java9后没有这个问题)
反射
什么是反射
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射机制的优缺点
优点:能够运行时动态获取类的实例,提高灵活性;可与动态编译结合Class.forName('com.mysql.jdbc.Driver.class'),加载MySQL的驱动类。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflectASM工具类,通过字节码生成的方式加快反射速度。
如何获取反射中的Class 对象
-
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
Class clz = Class.forName("java.lang.String"); -
类名.class。这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class; -
对象名.getClass()。
String str = new String("Hello"); Class clz = str.getClass(); -
如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。
反射的使用步骤
- 获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
- 调用 Class 类中的方法,既就是反射的使用阶段。
- 使用反射 API 来操作这些信息。
具体可以看下面的例子:
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
// 1、获取类的Class对象实例
Class clz = Class.forName("com.chenshuyi.api.Apple");
// 2、根据Class对象实例获取Constructor对象
Constructor appleConstructor = clz.getConstructor();
// 3、根据Constructor对象的newInstance方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
// 4、获取方法的Method对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
// 5、利用invoke方法调用方法
setPriceMethod.invoke(appleObj, 14);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
}
}
从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:
Apple Price:5
Apple Price:14
反射机制的原理
深入理解java反射原理 - 阿牛20 - 博客园 (cnblogs.com)
反射的应用场景
- 动态代理(Spring中xml的配置模式。)
- 注解
模拟 Spring 加载 XML 配置文件:
public class BeanFactory {
private Map<String, Object> beanMap = new HashMap<String, Object>();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;
//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
//获取其属性描述
java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
//设置值的方法
Method mSet = null;
//创建一个对象
Object obj = bean.newInstance();
//遍历该bean的property属性
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element foo2 = (Element) ite.next();
//获取该property的name属性
Attribute name = foo2.attribute("name");
String value = null;
//获取该property的子元素value的值
for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
Element node = (Element) ite1.next();
value = node.getText();
break;
}
for (int k = 0; k < pd.length; k++) {
if (pd[k].getName().equalsIgnoreCase(name.getText())) {
mSet = pd[k].getWriteMethod();
//利用Java的反射极致调用对象的某个set方法,并将值设置进去
mSet.invoke(obj, value);
}
}
}
//将对象放入beanMap中,其中key为id值,value为对象
beanMap.put(id.getText(), obj);
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
//other codes
}
注解
什么使注解,有什么作用
注解是一种特殊的注释,用于修饰方法、类、变量,提供信息在运行时使用,具体作用:
- 生成文档
- 编译检查
- 编译时处理,例如动态生成代码
- 运行时处理,例如反射创建对象
注解的运行方法
注解在解析后才会生效,分为两种:
- 编译时解析:在编译时进行检查,例如@Override
- 运行时解析:例如Spring提供的注解,@component,在运行时通过反射处理
泛型
什么是泛型
定义:泛型是jdk5引入的新特性,可以将数据类型作为参数传递
作用:增强代码的可读性和可靠性
使用:泛型类,泛型接口,泛型方法
注意:静态方法无法使用类所接受的泛型
项目中哪里用到了泛型
- 定义自定义返回结果接口
- 定义通用方法工具类
如何定义一个泛型方法
传入参数Class<T>~类对象,返回值前加上<T>表明泛型方法,在方法内部使用newInstance方法获取实例对象
泛型的上限和下限
List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List或List。
在使用泛型时可以指定泛型的上限和下限
- 上限:
T extends Number表示数字类型的子类 - 下限:
Info<? super String> temp表示只能接受字符串和Object
为什么说java泛型是伪泛型
在jdk1.5引入了泛型擦除,就是在编译阶段java会将所有泛型转为具体的类型,在运行时就跟没有泛型一样
序列化
什么是序列化,什么是反序列化
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将序列化生成的二进制字节流转换成数据结构或对象的过程
常见应用场景:
- 网络传输
- 存储到数据库
- 存储到文件(IO)
- 存储到内存
哪些字段不会被序列化
- **transient修饰的变量:**在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,
transient变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。 - static修饰的变量: 因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
常见的序列化协议
- 基于二进制流:Hession,Kryo,ProtoBuf,ProtoStuff
- 基于文本:json,xml
为什么不推荐使用JDK自带的序列化技术
- 不支持跨语言
- 性能差,生成的序列化文件大
- 存在安全问题
异常
Error和Exception区别是什么
两者都继承Throwable类
- Exceptiton:程序本身可以处理的异常,可以被try-catch捕获
- Error:程序本身无法处理的异常,例如虚拟机错误(
Virtual MachineError)、内存溢出(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等等
CheckedException和UnCheckedException有什么区别
CheckedException和UnCheckedException都继承自Exception类
-
CheckedException:受检查的异常,在编译时会对其检查,如果没有做异常处理则无法通过编译。常见的受检查异常有:IO 相关的异常、
ClassNotFoundException、SQLException... -
UnCheckedException:不受检查的异常,即使不做处理也能通过编译,一般就是RuntimeException,常见的有:
-
NullPointerException(空指针错误) -
IllegalArgumentException(参数错误比如方法入参类型错误) -
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类) -
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)SecurityException(安全错误比如权限不够)UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
-
Throwable常用方法
String getMessage:获取异常发生的简要信息String toString:获取异常的详细信息String getLocalizedMessage:获取本地化信息,如果没重写,跟get Message是一样的void printStackTrace: 打印异常信息
finally中可以写return吗
不行,会导致try或catch中的return语句失效
finally一定会执行吗
以下情况不会执行:
- 虚拟机中断
- 线程死亡
- CPU关闭
throw和throws的区别
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
异常使用有哪些需要注意的地方
- 不要把异常定义为静态变量,会导致异常栈信息错乱
- 抛出的异常要有意义
- 尽量使用更为具体的异常
- 打印日志后就不要抛异常
JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
IO
如何从数据传输方式理解IO流
从数据传输角度,IO流可以分为:
- 字节流:读取单个字节,常用于处理二进制数据,例如图片、视频等等
- 字符流:读取单个字符,常用于处理文本数据,例如文本文件(本质也是二进制,但采用某种编码使得人可以阅读)
IO流的四个抽象类基类
- InputStream:输入字节流
- OutputStream:输出字节流
- Reader:输入字符流
- Writer:输出字符流
IO流为什么要分字节流和字符流
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 虚拟机将字节转换为字符的过程比较耗时
- 如果不知道编码类型,字节流很容易出现乱码问题
随机访问流
随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile 。
RandomAccessFile 的构造方法如下,我们可以指定 mode(读写模式)。
// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{
// 省略大部分代码
}
读写模式主要有下面四种:
r: 只读模式。rw: 读写模式rws: 相对于rw,rws同步更新对“文件的内容”或“元数据”的修改到外部存储设备。rwd: 相对于rw,rwd同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
IO中有哪些设计模式
-
装饰器模式:可以在不改变原有对象的情况下拓展功能;适用于继承关系非常复杂的场景(相当于不用继承就获得核心功能);例如:
BufferedInputStream(字节缓冲输入流)创建时可以传入FileInputStream,从而增加缓冲功能new BufferedInputStream(new FileInputStream(inputStream)); -
适配器模式:让接口不兼容的类协调工作,例如将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
Reader reader = new InputStreamReader(inputStream); -
工厂模式:用于创建对象,NIO 中大量用到了工厂模式,比如
Files类的newInputStream方法用于创建InputStream对象(静态工厂)、Paths类的get方法创建Path对象(静态工厂) -
观察者模式:NIO中的文件目录监听用到了观察者模式,
什么是同步和阻塞?
- 阻塞和非阻塞:程序级别的,描述程序在访问IO资源时,如果IO资源还没有准备好,前者等待,后者继续执行,并且使用线程不断询问直到准备好
- 同步和非同步:操作系统级别的,描述操作系统在收到程序访问IO请求时,如果IO资源还没准备好,前者不响应直到资源准备后,后者会返回一个标记(后续进行通知的地方),等资源准备好后再用事件机制返回给程序
什么是多路复用IO
系统调用由多个任务组成,将这些任务拆分执行
什么是信号驱动IO
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
BIO、NIO 和 AIO 的区别?
BIO:同步阻塞,线程会一直等待阻塞直到IO操作结束
NIO:同步非阻塞,线程会先询问是否准备就绪,如果没有准备好先去做别的事,过会儿再来问
AIO:异步非阻塞,线程发起IO请求后就返回,让别的线程去做IO操作,操作完了再通知
什么是零拷贝,有什么用
传统IO读写数据需要内核空间和用户空间来回复制,在高并发场景下,性能开销比较大
零拷贝的实现
mmap + write:
- 应用程序调用
mmap()后,DMA会将磁盘的数据拷贝到内核缓冲区,接着做映射,使应用程序和内核共享缓冲区 - 应用程序调用
write()后,CPU会直接将内核缓冲区的数据拷贝到socket缓冲区,从而在内核中实现了直接拷贝 - 最后由socket将数据DMA拷贝到网卡中
这样操作减少了内核态到用户态的拷贝次数(3次),但仍然由CPU搬运数据,过程包含两次系统调用,四次上下文切换
sendfile
- 网卡不支持 SG-DMA:过程与mmap + write类似,同样由CPU进行拷贝,不过只进行了一次系统调用,两次上下文切换
- 网卡支持SG-DMA:DMA会将磁盘的数据拷贝到内核缓冲区,网卡使用SG-DMA方式直接从内核缓冲区拷贝数据;这种方式只有两次拷贝,没有在内存空间中拷贝数据,实现了真正的零拷贝