java 提供了哪些IO方式? NIO如何实现多路复用?
BIO
NIO
AIO
NIO的多路复用实现:
NIO(New I/O)提供高性能数据操作的原理是基于非阻塞I/O和事件驱动的模型。
NIO 使用选择器( Selector )来实现非阻塞 I/O,它允许一个线程同时管理多个通道(Channel),
监听多个通道上的事件,如数据可读、数据可写等。
这个模型使得单个线程能够有效地管理多个连接,减少了线程开销,从而提供了高性能的I/O操作。
BIO 引入线程池
线程实现是比较重量级的,启动或者销毁一个线程是有明显开销的,
每个线程都有单独的线程栈等结构,需要占用非常明显的内存,所以,每一个 Client 启动一个线程似乎都有些浪费。
通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建、销毁线程的开销,这是我们构建并发服务的典型方式
工作方式,可以参考下图来理解。
当连接数量上升时:
线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
NIO 多路复用的模式
可以看到,在前面两个样例中,IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 ****NIO 则是利用了单线程 轮询 事件的机制,通过高效地定位就绪的 Channel ,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。下面这张图对这种实现思路进行了形象地说明
AIO
Java 7 引入的 NIO 2 中,又增添了一种额外的异步 IO 模式,利用事件和 回调 ,处理 Accept、Read 等操作
基础 API 功能与设计, InputStream/OutputStream 和 Reader/Writer 的关系和区别。
InputStream和OutputStream是用于处理字节流,而Reader和Writer用于处理字符流。字符流是基于字节流的高级抽象,用于处理字符数据时更方便和安全。- 字符流在处理字符数据时能够正确处理字符编码,而字节流不会。这是因为字符可以使用多个字节表示,而字符流会自动处理字符编码转换。
- 通常,当您需要读写文本文件时,建议使用
Reader和Writer,因为它们更适合处理字符数据,而不需要手动处理字符编码。而当需要处理二进制文件(如图像、音频、视频等)时,可以使用InputStream和OutputStream。
总之,InputStream和OutputStream用于处理字节数据,而Reader和Writer用于处理字符数据,并且后者更适合处理文本文件。在选择时,要根据您的需求和处理的数据类型来决定使用哪种类型的流。
Java有几种文件拷贝方式?哪一种最高效?
- 利用 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);
}
}
}
- 利用 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); sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
- Java 标准类库本身已经提供了几种 Files.copy 的实现
copy效率:
对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
扩展:
拷贝实现机制分析:
先来理解一下,前面实现的不同拷贝方法,本质上有什么明显的区别。
首先,你需要理解用户态空间(User Space) 和内核态空间( Kernel Space),
这是操作系统层面的基本概念,操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;
而用户态空间,则是给普通应用和服务使用。
当我们使用输入输出流进行读写时,
实际上是进行了多次上下文切换,
比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。
这种方式会带来一定的额外开销,可能会降低 IO 效率。
而基于 ****NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到零拷贝 技术,
数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。
注意,transferTo 不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket 发送,同样可以享受这种机制带来的性能和扩展性提高。
transferTo 原理图:
而最常见的copy方法底层也是用的用户态拷贝实现:
我们明确这个最常见的 copy 方法其实不是利用 transferTo,而是本地技术实现的用户态拷贝。
总结
如何提高类似拷贝等 IO 操作的性能,
有一些宽泛的原则:
-
在程序中,使用缓存等机制,合理减少 IO 次数(在网络通信中,如 TCP 传输,window 大小也可以看作是类似思路)。
-
使用 transferTo 等机制,减少上下文切换和额外 IO 操作。
-
尽量减少不必要的转换过程,比如编解码;对象序列化和反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制信息转换成字符串,直接传输二进制信息。
Direct Buffer
使用 Direct Buffer,我们需要清楚它对内存和 JVM 参数的影响。首先,因为它不在堆上,所以 Xmx 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,
我们可以使用下面参数设置大小:
-XX:MaxDirectMemorySize=512M
从参数设置和内存问题排查角度来看,
这意味着我们在计算 Java 可以使用的 内存 大小的时候,不能只考虑堆的需要,还有 Direct Buffer 等一系列堆外因素。如果出现内存不足,堆外内存占用也是一种可能性 。
跟踪和诊断 Direct Buffer 内存占用?
因为通常的垃圾收集日志等记录,并不包含 Direct Buffer 等信息,所以 Direct Buffer 内存诊断也是个比较头疼的事情。幸好,在 JDK 8 之后的版本,我们可以方便地使用 Native Memory Tracking(NMT)特性来进行诊断,你可以在程序启动时加上下面参数:
-XX:NativeMemoryTracking={summary|detail}
注意,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,请谨慎考虑。
Java为什么不支持多继承?
Java 不支持多继承是为了简化编程语言和降低复杂性。
多继承指的是一个类可以从多个父类继承属性和方法。
尽管多继承在某些情况下可能非常强大,但它也带来了一些复杂性和潜在的问题,其中包括:
-
Diamond继承问题: 多继承可能导致“菱形继承”问题,也称为“Diamond继承问题”。
- 这种情况发生在一个子类继承自两个具有共同父类的类,然后尝试访问从这两个父类继承的相同方法或属性时。这种情况下编译器无法确定应该使用哪个父类的方法或属性,因此会产生歧义。
-
复杂性增加: 多继承会增加代码的复杂性,因为它涉及到解决继承冲突、多个父类之间的交互等问题。这可以使代码更难理解、维护和调试。
-
继承链变长: 多继承可能导致继承链变得非常复杂,这会使代码变得更加脆弱,因为任何一个父类的改变都可能影响到多个子类。
为了避免这些问题,Java采用了单继承的模型,其中每个类只能直接继承自一个父类。
这有助于提高代码的可读性、可维护性和可靠性。
为了弥补单 继承 的限制,Java引入了接口(interface)的概念,允许类实现多个接口,从而达到某种程度上的多继承效果,但接口只包含方法的签名,不包含方法的实现,因此避免了继承冲突和Diamond继承问题。
总之,Java不支持多继承是一种权衡,旨在使编程更加简单和可控。
什么是菱形继承?
"菱形继承"(Diamond Inheritance)是一种继承关系中的特殊情况,也称为 "菱形问题" 或 "菱形继承问题"。
它发生在一个类继承了两个类,而这两个类又都继承自同一个共同的父类。这种情况会导致一些潜在的问题,主要是二义性。
让我们通过一个示例来说明:
假设有一个类 A,然后有两个子类 B 和 C,它们都继承自 A,如下所示:
A
/ \
B C
\ /
D
现在,如果有一个类 D,它同时继承自 B 和 C,那么当你在类 D 中尝试访问来自父类 A 的某个方法或属性时,就会出现歧义。编译器不知道应该使用哪个父类 A 中的方法或属性,因为它们都可以被继承。这就是所谓的二义性,也就是菱形继承问题的核心。
为了解决菱形继承问题,编程语言需要提供一种机制来明确指定使用哪个父类的方法或属性。在Java中,这个问题通过接口(interface)来解决。类 B 和 C 可以实现一个共同的接口,而类 D 可以继承自类 B 和 C,从而避免了二义性。
这是一个经典的多继承问题,而Java选择了单继承和接口的方式,以降低复杂性和解决这类问题。
谈谈接口和抽象类有什么区别?
接口
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 ****API 定义和实现分离的目的。
接口,不能实例化;
不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;
同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。
Java 标准类库中,定义了非常多的接口,比如 java.util.List。
抽象类
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。
除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。
抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。
Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
扩展
面向对象设计
基本要素:封装、继承、多态。
面向对象的设计原则
S.O.L.I.D 原则
单一职责(Single Responsibility)
类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
开关原则(Open-Close, Open for extension, close for modification)
设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。
里氏替换(Liskov Substitution)
这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类 或者基类的地方,都可以用子类替换。
接口分离(Interface Segregation)
我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
依赖反转(Dependency Inversion)
实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。
谈谈你知道的设计模式?
设计模式是人们为软件开发中相同表征的问题,抽象出的可重复利用的解决方案.
大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式。
创建型模式
是对对象创建过程的各种问题和解决方案的总结,
包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
结构型模式
是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。
常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
行为型模式
是从类或对象之间交互、职责划分等角度总结的模式。
比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。
synchronized和ReentrantLock有什么区别呢?
synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。
ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。
再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。
与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,
比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。
synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
线程安全是一个多线程 环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,
线程安全需要保证几个基本特性:
原子性
简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性
是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性
是保证线程内串行语义,避免指令重排等。