十二 . Java 提供了哪些IO方式?NIO如何实现多路复用?
IO是软件开发中的核心部分之一,伴随着海量数据增长和分布式系统的发展,IO扩展能力愈发重要。
12.1 典型回答:
Java IO 方式有很多种,基础于不同的IO抽象模型和交互方式,可以进行简单区分。
12.1.1 传统的java.io包:
基于流模型实现,提供了我们最熟知的一些IO功能,如:File抽象,输入输出流等,交互方式是同步,阻塞的方式,在读取输入流或者写入输出流时,在读,写动作完成之前,线程会一直阻塞在哪里,调用是可靠的线性顺序。
好处是代码简单,直观,但是IO效率和扩展性成为了性能瓶颈。
12.1.2 Java 1.4中引入NIO(java.nio包):
提供了Channel,Selector,Buffer等新的抽象,可以构建多路复用的,同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方式
12.1.2 .1 详细解释:
- Channel(通道):Channel是NIO中的基本抽象,它表示与实体(如文件或套接字)的连接。通过使用Channel,可以更有效地进行读取和写入操作。例如,可以使用FileChannel从文件中读取数据,或者使用SocketChannel从网络套接字中读取数据。
- Buffer(缓冲区):Buffer是一个对象,它提供了对数据的存储和访问。使用Buffer,可以更高效地处理数据。例如,可以创建一个ByteBuffer来读取或写入数据,而不需要每次都直接操作底层的数据流。
- Selector(选择器):Selector是一个可以用于监视多个Channel的对象,它可以在一个线程中同时处理多个Channel的IO事件。使用Selector,可以避免为每个Channel创建一个线程,从而提高程序的性能和资源利用率。
假设我们有一个服务器程序需要同时处理多个客户端的请求。如果使用传统的阻塞IO模型,每个客户端连接都需要一个独立的线程来处理。但是使用NIO的话,可以使用一个Selector来监视多个客户端连接的事件,如读取和写入,然后在一个线程中处理这些事件,从而减少了线程的数量。
12.1.2.2 多路复用的,同步非阻塞IO解释:
构建多路复用的、同步非阻塞IO程序是指通过使用NIO(New I/O)提供的抽象,可以实现同时处理多个IO操作而无需为每个IO操作创建一个独立的线程。这种方式可以在单个线程中处理多个IO操作,提高了程序的性能和资源利用率。
- 多路复用:传统的IO操作通常是阻塞的,即当一个IO操作进行时,其他的IO操作必须等待。而使用NIO的多路复用机制,可以在一个线程中同时监视多个IO通道(如SocketChannel),只有当某个IO通道准备就绪时才会进行相关的读取或写入操作。这样可以避免为每个IO通道创建一个线程,从而提高了程序的并发能力。
- 同步非阻塞:在NIO中,IO操作是同步的,也就是说程序会等待IO操作完成后再继续执行。不过,与传统的阻塞IO不同的是,NIO中的IO操作是非阻塞的。这意味着当进行一个IO操作时,如果数据未准备好或无法立即读取或写入,则不会阻塞程序的执行,而是立即返回给应用程序,并且应用程序可以继续执行其他任务。这种非阻塞的特性使得程序能够更高效地利用CPU资源。
因此,构建多路复用的、同步非阻塞IO程序可以通过使用NIO的Selector来监视多个IO通道的事件,通过Buffer来读取和写入数据,并通过Channel来进行实际的IO操作。这种方式可以使得一个线程能够同时处理多个IO操作,提高了程序的性能和资源利用率。
12.1.3 Java7,NIO的改进:NIO2(AIO)
引入了异步非阻塞IO方式,AIO(Asynchronous IO),异步IO操作,基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在哪里,当后台处理完成,操作系统会通知相应的线程进行后续工作。
12.1.3.1 详细解释:
Java中的NIO(New I/O)和AIO(Asynchronous I/O)时,它们是两种不同的IO模型,具有以下详细区别:
1.编程模型:
- NIO:NIO使用基于选择器(Selector)的事件驱动模型。在NIO中,应用程序将通道(Channel)注册到选择器,并通过选择器监听通道上的事件(如读、写等)。然后,应用程序可以通过轮询选择器来检查哪些通道已经就绪,并进行相应的操作。
- AIO:AIO使用异步回调机制,称为CompletionHandler。在AIO中,应用程序发起IO操作后立即返回,并提供一个回调函数或回调方法来处理操作完成的结果。当操作完成时,操作系统会通知应用程序执行相应的回调。
2.阻塞与非阻塞:
- NIO:NIO属于同步非阻塞IO模型。当进行IO操作时,如果数据尚未准备好或无法立即读取或写入,则不会阻塞程序执行。NIO使用"就绪选择"(Ready Selection)机制来确定通道是否可读或可写。
- AIO:AIO是异步非阻塞IO模型。在进行IO操作后,应用程序可以立即返回而不会阻塞。当操作完成后,操作系统会通知相应的线程或进程进行后续处理。
3.调用方式:
- NIO:NIO使用Channel、Buffer和Selector等抽象对象来进行IO操作。应用程序需要调用相关方法来管理和处理通道上的读写操作。
- AIO:AIO使用异步回调机制。应用程序通过提供回调函数或回调方法来处理IO操作的结果,当操作完成时,操作系统会调用相应的回调。
适用场景:
- NIO:NIO适合处理大量的小规模连接和低延迟要求的场景,如服务器端的网络编程。它通过一个线程处理多个通道,从而提高了程序的并发性和资源利用率。
- AIO:AIO适合处理较少的连接但每个连接需要处理大量数据的场景,如高吞吐量的网络编程。它通过异步回调机制可以更好地应对大量IO操作同时进行的情况。
总结来说,NIO和AIO是两种不同的IO模型。NIO使用选择器和轮询方式来实现事件驱动,适用于处理大量小规模的连接;而AIO使用异步回调机制来处理IO操作结果,适用于处理较少但需处理大量数据的连接。开发者可以根据具体的需求和场景选择合适的IO模型。
12.1.3.2 偏底层解释:
AIO(Asynchronous IO)模型相对于NIO(Non-blocking IO)模型来说,不太适合处理较多连接的场景有以下几个原因:
1.系统资源占用:AIO模型在处理每个连接时需要注册回调函数,并且需要维护回调函数的执行状态。当连接数很大时,系统需要同时管理和跟踪大量的回调函数,这会增加系统的资源占用。而NIO模型使用选择器(Selector)来管理多个通道(Channel),可以通过单个线程处理多个连接,减少了系统资源的占用。
2.实现复杂性:AIO模型的实现相对复杂,需要支持异步操作和回调机制。这要求底层操作系统需要提供相应的AIO支持,并且编程框架也需要提供对AIO模型的良好封装和支持。目前并不是所有的操作系统和编程语言都对AIO提供了完全的支持,这使得AIO在某些平台上的可用性和易用性受到限制。
3.应用程序设计复杂性:使用AIO模型编写应用程序需要理解异步回调的概念和处理方式。开发者需要合理地设计回调函数的处理逻辑,以保证正确性和性能。相比之下,NIO模型相对更简单直观,可以使用事件驱动的方式处理IO事件。
AIO模型在面对较多连接的场景时存在系统资源占用和实现复杂性的问题。相比之下,NIO模型更适合处理大量连接的情况,能够更好地管理多个连接并提高系统的并发性能和资源利用率。因此,在需要处理较多连接的情况下,通常会选择使用NIO而不是AIO。
12.2 考点分析:
利用了BIO,NIO,NIO2(AIO)进行分类解释,可能会继续深入进行提问,实现原理这些,存在的问题和改进的想法。
12.3 小结:
12.3.1 区分同步和异步(synchronous/asynchronous):
同步是可靠的有序运行机制,当进行同步操作的时候,后续的任务是等待当前调用返回,才会进入下一步,但是异步操作相反,其他任务不需要等待当前调用返回,依靠事件,回调机制进行实现任务间次序关系。
12.3.2 区分阻塞与非阻塞(blocking/non-blocking):
进行阻塞操作的时候,当前线程会处于阻塞状态,无法执行其他任务,只有当条件就绪才能继续,如:ServerSocket新连接简历完成,或者,数据读取,写入完成。
非阻塞则是不考虑IO操作是否结束,直接进行返回,相应的操作在后台继续进行处理。
12.3.3 趣谈例子:
BIO,快递员通知你有一份快递会在今天送到某某地方,你需要在某某地方一致等待快递员的
到来。
NIO,快递员通知你有一份快递会送到你公司的前台,你需要每隔一段时间去前台询问是否有
你的快递。
AIO,快递员通知你有一份快递会送到你公司的前台,并且前台收到后会给你打电话通知你过
来取。
十三 . Java有几种拷贝方式?哪一种最高效?
13.1 典型回答
13.1.1 java.io类库进行拷贝:
利用java.io 类库,直接为源文件构建一个FileInputStream读取,然后为目标文件构建一个FileOutputStream完成写入工作。
public static void copyFileByStream(File source, File dest) throws IOException { try (InputStream is = new FileInputStream(source); OutputStream os = new FileOutputStream(dest);){ byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } }
用于通过流复制文件。该方法接受两个参数:源文件(source)和目标文件(dest)。在复制文件的过程中可能会抛出IOException异常。
方法的具体实现如下:
- 使用InputStream类和FileInputStream类创建一个输入流(is),用于读取源文件。
- 使用OutputStream类和FileOutputStream类创建一个输出流(os),用于写入目标文件。
- 创建一个大小为1024字节的缓冲区(byte[]),用于临时存储从输入流读取的数据。
- 使用while循环,不断从输入流中读取数据,直到读取完毕(length = is.read(buffer))。
- 在每次读取数据后,使用输出流将数据写入目标文件(os.write(buffer, 0, length))。
- 最后,关闭输入流和输出流。使用try-with-resources语句可以确保在方法执行完毕后自动关闭流,无需手动处理。
13.1.2 java.nio 类库进行拷贝:
利用java.nio类库提供的transferTo或transferFrom方法实现。
public static void copyFileByChannel(File source, File dest) throws IOException { try (FileChannel sourceChannel = new FileInputStream(source) .getChannel(); FileChannel targetChannel = new FileOutputStream(dest).getChannel ();){ for (long count = sourceChannel.size() ;count>0 ;) { long transferred = sourceChannel.transferTo( sourceChannel.position(), count, targetChannel); s count -= transferred; } } }
用于通过通道复制文件。该方法接受两个参数:源文件(source)和目标文件(dest)。在复制文件的过程中可能会抛出IOException异常。
方法的具体实现如下:
1.使用FileChannel类和FileInputStream类创建一个源文件通道(sourceChannel),用于读取源文件。
2.使用FileChannel类和FileOutputStream类创建一个目标文件通道(targetChannel),用于写入目标文件。
3.使用for循环,从源文件通道中读取数据并写入目标文件通道,直到源文件通道没有剩余数据为止。
4.在每次迭代中,调用sourceChannel.transferTo()方法将数据从源文件通道传输到目标文件通道。
- sourceChannel.position()表示从当前位置开始传输数据。
- count表示要传输的字节数,初始值为源文件通道的大小。
- targetChannel表示目标文件通道。
- transferred表示实际传输的字节数。
5.将已传输的字节数从count中减去,以便进行下一次迭代。
6.最后,关闭源文件通道和目标文件通道。使用try-with-resources语句可以确保在方法执行完毕后自动关闭通道,无需手动处理。
13.2 小结:
NIO transferTo/From的方式可能更快,因为更可以利用现代操作系统底层机制,避免不必要的拷贝和上下文切换。
13.3 拷贝实现机制分析
上文提到实现的不同拷贝方法,本质的区别是什么。
从理解用户态空间(User Space),内核空间(Kernel Space),操作系统层面的基本概念,操作系统内核,硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。
当我们输入输出流进行读写的时候,实际上进行了多次上下文的切换,如:应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,在切换到用户态将数据从内核缓存读取到用户缓存。
写入操作同上。
所以,这种方式会带来额外的开销,可能会降低IO效率。
但是基于NIO transferTo 的实现方式,在Linux和Unix 上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能,transfer To 不仅仅是可以用在文件拷贝中,如:读取磁盘文件然后进行Socket发送,也可以享受这种机制带来的性能和扩展性提高
13.3.1 详细解释:
NIO的transferTo方法实现了零拷贝技术,在Linux和Unix系统上可以使用这种机制来提高应用程序的性能和扩展性。
传统的数据传输方式涉及到多次上下文切换和内存拷贝。比如,读取磁盘文件内容需要将数据从内核态复制到用户态的应用程序缓冲区,然后再将数据从应用程序缓冲区复制到内核态的Socket缓冲区。这个过程中包含了多次上下文切换和内存拷贝操作,对于大量的数据传输会导致性能开销和资源浪费。
而NIO的transferTo方法则避免了这些开销。它利用了操作系统提供的零拷贝技术,直接在内核空间中进行数据传输,不需要用户态的参与。具体实现上,当调用transferTo方法时,传输的数据会被直接发送到目标通道,无需经过用户态的应用程序缓冲区。这样就省去了上下文切换和不必要的内存拷贝,从而提高了传输性能。
举个例子来说,假设有一个应用程序需要将大文件从磁盘读取并通过网络发送到另一台机器。如果使用传统的方式,需要将文件内容从内核态复制到用户态应用程序缓冲区,然后再将数据从应用程序缓冲区复制到内核态Socket缓冲区。而使用NIO的transferTo方法,可以直接将文件内容从内核态传输到Socket缓冲区,避免了不必要的数据复制操作,提高了传输性能。
NIO的transferTo方法利用零拷贝技术实现了高效的数据传输,不仅适用于文件拷贝,还可以在其他场景中使用,如读取磁盘文件并通过Socket发送数据,从而在性能和扩展性上获得提升。
十四 . 谈谈接口和抽象的区别?
掌握面向对象设计原则和技巧,是保证高质量代码的基础之一。
14.1 典型回答
接口和抽象类是Java面向对象设计的两个基础机制。
接口是对行为的抽象,是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。
接口不能实例化,不能包含任何非常量的成员,任何field都是隐含着public static final 的意义,也没有非静态方法实现,要么是抽象方法,要么是静态方法。
抽象类是不能实例化的类,使用abstract 关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和一般Java类并没有很大区别,可以有一个或者多个抽象方法,也可以没有抽象方法,抽像类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。
Java类实现interface使用implements关键词,继承abstract class则是使用extends
14.1.1 接口(Interface)和抽象类(Abstract Class)例子
14.1.1.1 简单的接口
Animal接口定义了两个抽象方法eat()和sleep(),表示动物的行为。其他类可以通过实现Animal接口来具体实现这些行为。
public interface Animal { void eat(); void sleep(); }
14.1.1.2 简单的抽象类
Shape抽象类包含了一个抽象方法area()和一个非抽象方法draw()。area()方法用于计算形状的面积,而draw()方法用于绘制形状。其他类可以通过继承Shape抽象类来具体实现这些方法,并且可以使用抽象类中定义的成员变量x和y。
public abstract class Shape { protected int x; protected int y; public Shape(int x, int y) { this.x = x; this.y = y; } public abstract double area(); public void draw() { System.out.println("Drawing shape at (" + x + ", " + y + ")"); } }
14.1.1.3 接口,抽象具体应用
Java中,类实现接口使用implements关键字,继承抽象类则使用extends关键字。例如,如果要实现Animal接口和继承Shape抽象类。
Dog类实现了Animal接口,并实现了eat()和sleep()方法;Circle类继承了Shape抽象类,并实现了area()方法。这样,我们就可以根据需要实现自己的具体行为,并且可以复用接口或抽象类中定义的代码和规范。
public class Dog implements Animal { @Override public void eat() { System.out.println("Dog is eating"); } @Override public void sleep() { System.out.println("Dog is sleeping"); } } public class Circle extends Shape { private double radius; public Circle(int x, int y, double radius) { super(x, y); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } }
14.1.1.4 接口,抽象比较区别
接口(Interface)和抽象类(Abstract Class):
- 定义:接口是对行为的抽象,是一组抽象方法的集合,用于定义类应该具备的行为;而抽象类是一种不能被实例化的类,可以包含抽象方法以及非抽象方法。
- 实现方式:类实现接口使用implements关键字,一个类可以同时实现多个接口;类继承抽象类使用extends关键字,一个类只能继承一个抽象类。
- 构造函数:接口不能有构造函数,因为接口没有实例化的概念;抽象类可以有构造函数,并且在子类创建对象时会调用父类的构造函数。
- 成员变量:接口中只能定义常量,即public static final类型的变量;抽象类可以定义普通成员变量,也可以定义常量。
- 方法实现:接口中的方法都是抽象方法,不包含具体的实现代码,由实现接口的类提供具体实现;抽象类可以包含抽象方法和非抽象方法,非抽象方法可以有具体的实现代码。
- 单继承 vs 多实现:一个类可以继承一个抽象类,但只能实现多个接口。这是因为Java是单继承的,一个类只能有一个直接父类,但可以实现多个接口。
- 设计目的:接口主要用于实现多态和约束行为,强调"是什么";抽象类主要用于代码的重用和继承关系,强调"是一个"。
- 接口和抽象类并不是互斥的概念,它们在面向对象设计中有不同的应用场景和目的。根据具体的需求,选择使用接口还是抽象类来实现不同的设计目标。
14.1.1.5 实例化的概念
实例化是指根据类的定义创建该类的一个具体对象的过程,也可以说是将类转化为对象的过程。在面向对象编程中,类是一种抽象的概念,描述了对象的属性和行为,而对象则是类的一个具体实例,具有具体的属性值和可以执行的方法。
实例化一个类的过程:
- 创建对象:使用关键字
new
来创建一个类的实例。例如,如果有一个叫做Person
的类,我们可以通过Person person = new Person();
来实例化一个Person
对象。 - 分配内存空间:在内存中为对象分配一块存储空间,用于存储对象的属性值。
- 初始化属性:根据类的定义,对对象的属性进行初始化。这可能涉及到构造函数的调用和属性的赋值操作。
- 返回对象引用:将对象的引用返回给变量或表达式,使得可以通过该引用访问和操作对象的属性和方法。
通过实例化一个类,我们可以创建多个相同类型的对象,并且每个对象都是独立的,具有自己的属性值和可以执行的方法。这样就能够灵活地使用和操作对象的数据和行为,实现面向对象编程的特性。
14.1.1.6 为什么接口没有实例化的概念?
接口没有实例化的概念是因为接口本身只是一种规范或契约,用于定义类应该具备的行为,而不关心具体的实现细节。
接口定义了一组抽象方法,这些方法只有方法签名,没有具体的实现代码。接口的目的是为了使得不同的类能够按照相同的契约来提供特定的行为,实现代码的复用和统一的接口规范。
由于接口中所有的方法都是抽象的,没有具体的实现代码,因此无法直接实例化一个接口对象。接口只是一种约束或规范,它描述了类应该具备的行为,但不提供具体的对象实例。
要使用接口,需要创建一个类来实现(implement)该接口,并提供接口中定义的所有抽象方法的具体实现。通过类实现接口,可以创建类的对象,从而具体地实现了接口所定义的行为。
因此,接口没有实例化的概念,它只起到一种约束作用,定义了类应该具备的行为规范,需要通过类来实现这些行为并创建对象。
14.1.1.7 为什么抽象类是一种不能被实例化的类?
抽象类是一种不能被实例化的类,这是因为抽象类本身存在着未实现的抽象方法,无法完整地描述一个具体对象的状态和行为。
抽象类通过使用abstract关键字进行声明,在抽象类中可以包含抽象方法(没有方法体)和非抽象方法(有具体的方法体)。抽象方法是指只有方法签名而没有实际实现的方法。由于抽象类中存在抽象方法,所以抽象类本身并不能提供一个完整的可实例化的对象。
抽象类的设计目的主要是为了作为其他具体子类的基类或模板类,它定义了一组共享的属性和方法,但需要子类来实现其中的抽象方法才能构成完整的类。抽象类充当了一种规范或者约定,它规定了子类应该具备的共同特征和具体实现。
由于抽象类存在着未实现的抽象方法,无法提供一个完整的具体对象,所以抽象类不能直接被实例化。我们只能通过创建一个继承自抽象类的具体子类,并在子类中实现父类的抽象方法,然后通过子类来创建对象实例。这样才能得到一个具有完整实现的、可实例化的对象。
14.2 考点分析:
可以全面考察你对基本机制的理解和掌握。
如:
1.Java的基本元素的语法是否理解准确。
2.能否定义出语法正确的接口和抽象类
3.对于继承,涉及到重载(Overload),重写(Override)
4.使用的场景和掌握设计方法。