What is Java ?(四)
What is Java ?(四)
xiaoyan内容:序列化
、I/O
、其他
本篇博客是笔者作为初学者记录自己对Java一些基本概念的理解。内容参考了大量网络资源,篇幅很长,旨在作为个人学习笔记,供自己日后回顾和复习。
序列化
怎么把一个对象从一个JVM转移到另一个JVM?
- 使用序列化和反序列化:将对象序列化为字节流(objectoutputstream),发送到另一个JVM中再进行字节流反序列化(objectinputstream)。
- 使用消息传递机制:可以使用消息队列等方式,通过网络传输对象。常见的消息队列系统包括 RabbitMQ、Kafka 等。
- 远程调用(RPC):使用远程调用框架(如 RMI、gRPC、Dubbo 等)进行跨 JVM 的对象调用。
- 使用数据共享:使用数据共享,如将对象所需的数据存入数据库或共享缓存中,能够让其他JVM访问得到。
序列化和反序列化有没有更好的设计?
Java默认的序列化虽然方便,但也存在一些问题:
- 无法跨语言:Java序列化格式是Java特有的,它依赖于Java的内部数据结构和类型系统。因此,生成的序列化数据无法直接被其他编程语言(如Python、C++、JavaScript等)解析和使用。这意味着如果你需要在不同的编程语言之间共享数据,Java序列化机制就无法满足需求。为了实现跨语言的互操作性,通常需要使用更通用的序列化格式,如JSON、XML、Protocol Buffers、Avro等。
- 容易被攻击:Java序列化机制存在安全漏洞,尤其是在反序列化过程中。攻击者可以通过构造恶意的序列化数据,在反序列化时执行任意代码,从而导致安全问题。这种攻击被称为“反序列化漏洞”或“反序列化攻击”。为了防止这种攻击,开发者需要非常小心地处理反序列化过程,或者使用更安全的序列化机制,如JSON、Protocol Buffers等,这些机制通常不会引入类似的安全风险。
- 性能差:Java序列化机制在性能方面表现不佳,尤其是在处理大量数据时。主要原因包括:
- 序列化后的数据体积较大:Java序列化生成的数据通常比其他序列化格式(如Protocol Buffers)更大,这会导致更高的网络传输成本和存储成本。
- 序列化和反序列化过程较慢:Java序列化机制在处理复杂对象时,性能开销较大。尤其是在需要频繁进行序列化和反序列化操作的场景中,性能问题会更加明显。
更好的设计
为了解决上述问题,可以使用以下几种替代方案:
1. JSON 序列化
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写,同时也易于解析和生成。许多编程语言都支持JSON序列化,因此可以实现跨语言的数据交换。
示例代码(使用 Jackson 库)
1 | import com.fasterxml.jackson.databind.ObjectMapper; |
2. Protocol Buffers
Protocol Buffers(ProtoBuf)是Google开发的一种轻量级、高效的序列化格式,支持多种编程语言,并且具有更好的性能和安全性。
示例代码(使用 Protocol Buffers)
首先,定义一个 .proto
文件:
1 | syntax = "proto3"; |
然后,生成Java类并使用:
1 | import com.example.MyObject; |
3. Thrift
Apache Thrift 是另一种高效的序列化框架,支持多种编程语言,并且具有良好的性能和扩展性。
示例代码(使用 Thrift)
首先,定义一个 .thrift
文件:
1 | namespace java com.example |
然后,生成Java类并使用:
1 | import org.apache.thrift.TException; |
为了解决Java默认序列化机制存在的问题,可以使用JSON、Protocol Buffers和Thrift等替代方案。这些方案具有更好的跨语言支持、更高的安全性和更好的性能,适用于不同的应用场景。
I/O
BIO、NIO、AIO:Java I/O模型的演进
在Java编程中,I/O操作是不可或缺的一部分。随着技术的发展,Java提供了多种I/O模型,以满足不同场景下的需求。本文将简要介绍三种主要的I/O模型:BIO(Blocking I/O)、NIO(Non-blocking I/O)和AIO(Asynchronous I/O)。
1. BIO(Blocking I/O)
BIO是Java传统的I/O模型,基于java.io
包实现。它通过字节流(Byte Stream)和字符流(Character Stream)来处理数据。BIO的核心特点是同步阻塞,即在进行I/O操作时,线程会被阻塞,直到操作完成。这种模型的执行顺序是线性的,代码编写简单直观,但处理效率较低,扩展性有限。
优缺点:
代码简单,易于理解和维护;适用于简单的I/O操作场景。
处理效率低,尤其是在高并发环境下,线程阻塞会导致资源浪费;扩展性差,难以应对大规模并发请求。
2. NIO(Non-blocking I/O)
NIO是Java 1.4引入的一种新型I/O模型,旨在解决BIO在高并发环境下的性能瓶颈。NIO的核心思想是同步非阻塞,通过以下三大组件实现:
- 通道(Channel):类似于流,但支持双向数据传输。
- 缓冲区(Buffer):用于存储数据的容器,支持直接内存操作,提高数据处理效率。
- 多路复用器(Selector):用于管理多个通道,实现非阻塞I/O操作。
NIO通过Selector机制,允许单个线程管理多个通道,从而避免了线程阻塞,提高了系统的并发处理能力。
优缺点:
非阻塞I/O操作,提高了系统的并发处理能力;适用于高并发、低延迟的网络应用。
代码复杂度增加,需要理解Channel、Buffer和Selector等概念;调试和维护相对困难。
3. AIO(Asynchronous I/O)
AIO是NIO的进一步演进,提供了异步非阻塞的I/O操作方式。在AIO模型中,当发起I/O操作后,线程不会被阻塞,而是继续执行其他任务,I/O操作由后台线程处理完成后,通过回调机制通知主线程。
AIO的核心组件包括:
- 异步通道(Asynchronous Channel):支持异步I/O操作的通道。
- CompletionHandler:用于处理I/O操作完成后的回调。
优缺点:
异步非阻塞,进一步提高了系统的并发处理能力;适用于需要高并发、高吞吐量的应用场景。
实现复杂,需要处理回调和异步编程的复杂性;对开发者的技术要求较高。
NIO原理详解
NIO(Non-blocking I/O)是Java 1.4引入的一种新型I/O模型,旨在提高I/O操作的效率和系统的并发处理能力。NIO的核心思想是同步非阻塞,通过以下三大组件实现:
1. 核心组件
Selector(选择器/多路复用器):Selector是NIO同步机制的核心。它负责轮询多个Channel,检查它们是否准备好进行I/O操作(如读、写等)。Selector的引入避免了线程在等待I/O操作时的阻塞,从而提高了系统的并发处理能力。
Channel(通道):Channel类似于传统的流(Stream),但支持双向数据传输。与流不同的是,Channel可以直接与Buffer进行数据交换,无需线程等待。常见的Channel类型包括
FileChannel
、SocketChannel
和ServerSocketChannel
。Buffer(缓冲区):Buffer是用于存储数据的容器,支持直接内存操作,提高了数据处理的效率。Buffer有多种类型,如
ByteBuffer
、CharBuffer
等,分别用于处理不同类型的数据。
2. 工作原理
NIO的工作原理可以概括为以下几个步骤:
注册Channel到Selector:首先,将需要监听的Channel注册到Selector上,并指定感兴趣的事件(如连接、读、写等)。
Selector轮询:Selector会定期轮询所有注册的Channel,检查它们是否准备好进行I/O操作。如果某个Channel准备好,Selector会返回一个SelectionKey,表示该Channel可以进行相应的I/O操作。
处理I/O操作:当Selector检测到某个Channel准备好进行I/O操作时,应用程序可以通过SelectionKey获取对应的Channel,并进行读写操作。数据在Channel和Buffer之间进行传输,无需线程等待。
非阻塞机制:由于Selector的轮询机制,线程在等待I/O操作时不会被阻塞,可以继续处理其他任务。这种非阻塞机制大大提高了系统的并发处理能力。
3. 事件驱动机制
NIO采用了事件驱动机制,当某个I/O事件(如连接、读、写等)发生时,Selector会立即触发相应的事件处理逻辑,无需线程不断监视。这种机制减少了线程的空闲等待时间,提高了系统的响应速度。
4. 线程通信
在NIO中,线程间通过notify
和wait
机制进行通信,减少了线程切换的开销。当某个I/O操作完成时,线程可以通过notify
通知其他线程继续处理,避免了不必要的线程切换。
其他
代理模式和适配器模式有什么区别?
代理模式(Proxy Pattern)和适配器模式(Adapter Pattern)是两种常见的设计模式,它们在软件设计中有着不同的用途和实现方式。以下是它们的主要区别:
1. 定义和目的
代理模式
- 定义:代理模式为其他对象提供一个代理或占位符,以控制对这个对象的访问。
- 目的:主要用于控制对对象的访问,可以在不改变原始对象的情况下,增加额外的功能(如权限控制、延迟初始化、日志记录等)。
适配器模式
- 定义:适配器模式将一个类的接口转换成客户端所期望的另一个接口。
- 目的:主要用于解决接口不兼容的问题,使得原本由于接口不匹配而无法一起工作的类可以协同工作。
2. 结构和实现
代理模式
- 结构:代理模式通常包含一个代理类和一个真实主题类。代理类和真实主题类实现相同的接口,代理类内部持有真实主题类的引用。
- 实现:代理类在调用真实主题类的方法前后,可以执行额外的操作(如权限检查、日志记录等)。
适配器模式
- 结构:适配器模式通常包含一个适配器类、一个目标接口和一个被适配者类。适配器类实现目标接口,并在内部持有被适配者类的引用。
- 实现:适配器类将目标接口的方法调用转换为被适配者类的方法调用。
3. 使用场景
代理模式
- 场景:当需要控制对某个对象的访问时,可以使用代理模式。例如:
- 远程代理:控制对远程对象的访问。
- 虚拟代理:延迟加载对象,直到真正需要时才创建。
- 保护代理:控制对敏感对象的访问权限。
- 日志代理:在方法调用前后记录日志。
适配器模式
- 场景:当需要将一个类的接口转换为另一个接口,以便与现有代码兼容时,可以使用适配器模式。例如:
- 旧系统与新系统的接口不兼容。
- 第三方库的接口与现有代码不匹配。
- 需要复用已有的类,但其接口不符合需求。
集合多属性排序
假如有一个学生数组,想要按照成绩降序、学号升序排序,如何实现?
在Java中,可以使用Comparator
接口来实现集合的多属性排序。以下是一个示例,展示了如何对学生数组按照成绩降序、学号升序进行排序。
示例代码
1 | import java.util.Arrays; |
代码说明
Student类:定义了一个学生类,包含学号(
id
)、姓名(name
)和成绩(score
)三个属性。Main类:包含主方法,用于创建学生数组并进行排序。
排序逻辑:
- 使用
Arrays.sort
方法对学生数组进行排序。 - 通过
Comparator
接口实现多属性排序:- 首先按成绩降序排序(
Double.compare(s2.getScore(), s1.getScore())
)。 - 如果成绩相同,按学号升序排序(
Integer.compare(s1.getId(), s2.getId())
)。
- 首先按成绩降序排序(
- 使用
输出结果:排序后的学生数组将按照成绩降序、学号升序的顺序输出。
输出示例
1 | Student{id=104, name='David', score=92.5} |
native方法
native
方法是Java中的一种特殊方法,它使用native
关键字进行声明,表示该方法的实现是由非Java代码(通常是C/C++代码)提供的。native
方法允许Java程序调用底层操作系统或其他语言编写的库,从而实现Java与本地代码的交互。
volatile
和 synchronized
的区别
volatile
和 synchronized
是 Java 中用于处理并发问题的关键字,但它们的作用和使用场景有所不同。
1. volatile
作用:
- 保证可见性: 当一个线程修改了
volatile
变量的值时,其他线程能够立即看到这个修改。 - 禁止指令重排序:
volatile
变量的读写操作不会被 JVM 优化重排序,从而保证有序性。
适用场景:
- 单个变量的读写:
volatile
适用于单个变量的读写操作,特别是当这个变量被多个线程共享时。 - 状态标志: 例如,用于控制线程是否继续运行的标志变量。
代码示例:
1 | private volatile boolean flag = false; |
2. synchronized
作用:
- 互斥访问:
synchronized
关键字用于实现互斥访问,确保同一时刻只有一个线程可以执行被synchronized
修饰的代码块或方法。 - 保证可见性: 进入
synchronized
代码块或方法时,会清空工作内存中的变量副本,从主内存中重新加载;退出时,会将工作内存中的变量值刷新到主内存。
适用场景:
- 多个变量的读写:
synchronized
适用于多个变量的读写操作,特别是当这些变量需要保持一致性时。 - 复杂的同步逻辑: 例如,需要对多个操作进行同步的场景。
代码示例:
1 | private int count = 0; |
区别总结
特性 | volatile |
synchronized |
---|---|---|
作用 | 保证可见性、禁止指令重排序 | 互斥访问、保证可见性 |
适用场景 | 单个变量的读写、状态标志 | 多个变量的读写、复杂的同步逻辑 |
性能 | 相对较高,因为不需要加锁 | 相对较低,因为需要加锁 |
使用范围 | 仅限于变量 | 可以用于方法、代码块 |
原子性 | 不能保证复合操作的原子性 | 可以保证复合操作的原子性 |