What is Java ?(四)

内容:序列化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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonSerializationExample {
public static void main(String[] args) {
// 创建对象
MyObject obj = new MyObject("Hello, JSON!");

// 创建 ObjectMapper
ObjectMapper mapper = new ObjectMapper();

try {
// 序列化对象为 JSON 字符串
String jsonString = mapper.writeValueAsString(obj);
System.out.println("Serialized JSON: " + jsonString);

// 反序列化 JSON 字符串为对象
MyObject deserializedObj = mapper.readValue(jsonString, MyObject.class);
System.out.println("Deserialized Object: " + deserializedObj.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
}

class MyObject {
private String message;

public MyObject() {}

public MyObject(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

2. Protocol Buffers

Protocol Buffers(ProtoBuf)是Google开发的一种轻量级、高效的序列化格式,支持多种编程语言,并且具有更好的性能和安全性。

示例代码(使用 Protocol Buffers)

首先,定义一个 .proto 文件:

1
2
3
4
5
syntax = "proto3";

message MyObject {
string message = 1;
}

然后,生成Java类并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.example.MyObject;
import com.google.protobuf.InvalidProtocolBufferException;

public class ProtobufSerializationExample {
public static void main(String[] args) {
// 创建对象
MyObject.Builder builder = MyObject.newBuilder();
builder.setMessage("Hello, Protocol Buffers!");
MyObject obj = builder.build();

// 序列化对象为字节数组
byte[] data = obj.toByteArray();

try {
// 反序列化字节数组为对象
MyObject deserializedObj = MyObject.parseFrom(data);
System.out.println("Deserialized Object: " + deserializedObj.getMessage());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}

3. Thrift

Apache Thrift 是另一种高效的序列化框架,支持多种编程语言,并且具有良好的性能和扩展性。

示例代码(使用 Thrift)

首先,定义一个 .thrift 文件:

1
2
3
4
5
namespace java com.example

struct MyObject {
1: string message
}

然后,生成Java类并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TMemoryBuffer;
import org.apache.thrift.transport.TTransport;

public class ThriftSerializationExample {
public static void main(String[] args) {
// 创建对象
MyObject obj = new MyObject();
obj.setMessage("Hello, Thrift!");

try {
// 序列化对象为字节数组
TTransport transport = new TMemoryBuffer(1024);
TProtocol protocol = new TBinaryProtocol(transport);
obj.write(protocol);
byte[] data = transport.getBuffer();

// 反序列化字节数组为对象
TTransport transport2 = new TMemoryBuffer(data);
TProtocol protocol2 = new TBinaryProtocol(transport2);
MyObject deserializedObj = new MyObject();
deserializedObj.read(protocol2);
System.out.println("Deserialized Object: " + deserializedObj.getMessage());
} catch (TException e) {
e.printStackTrace();
}
}
}

为了解决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类型包括FileChannelSocketChannelServerSocketChannel

  • Buffer(缓冲区):Buffer是用于存储数据的容器,支持直接内存操作,提高了数据处理的效率。Buffer有多种类型,如ByteBufferCharBuffer等,分别用于处理不同类型的数据。

2. 工作原理

NIO的工作原理可以概括为以下几个步骤:

  1. 注册Channel到Selector:首先,将需要监听的Channel注册到Selector上,并指定感兴趣的事件(如连接、读、写等)。

  2. Selector轮询:Selector会定期轮询所有注册的Channel,检查它们是否准备好进行I/O操作。如果某个Channel准备好,Selector会返回一个SelectionKey,表示该Channel可以进行相应的I/O操作。

  3. 处理I/O操作:当Selector检测到某个Channel准备好进行I/O操作时,应用程序可以通过SelectionKey获取对应的Channel,并进行读写操作。数据在Channel和Buffer之间进行传输,无需线程等待。

  4. 非阻塞机制:由于Selector的轮询机制,线程在等待I/O操作时不会被阻塞,可以继续处理其他任务。这种非阻塞机制大大提高了系统的并发处理能力。

3. 事件驱动机制

NIO采用了事件驱动机制,当某个I/O事件(如连接、读、写等)发生时,Selector会立即触发相应的事件处理逻辑,无需线程不断监视。这种机制减少了线程的空闲等待时间,提高了系统的响应速度。

4. 线程通信

在NIO中,线程间通过notifywait机制进行通信,减少了线程切换的开销。当某个I/O操作完成时,线程可以通过notify通知其他线程继续处理,避免了不必要的线程切换。

其他

代理模式和适配器模式有什么区别?

代理模式(Proxy Pattern)和适配器模式(Adapter Pattern)是两种常见的设计模式,它们在软件设计中有着不同的用途和实现方式。以下是它们的主要区别:

1. 定义和目的

代理模式

  • 定义:代理模式为其他对象提供一个代理或占位符,以控制对这个对象的访问。
  • 目的:主要用于控制对对象的访问,可以在不改变原始对象的情况下,增加额外的功能(如权限控制、延迟初始化、日志记录等)。

适配器模式

  • 定义:适配器模式将一个类的接口转换成客户端所期望的另一个接口。
  • 目的:主要用于解决接口不兼容的问题,使得原本由于接口不匹配而无法一起工作的类可以协同工作。

2. 结构和实现

代理模式

  • 结构:代理模式通常包含一个代理类和一个真实主题类。代理类和真实主题类实现相同的接口,代理类内部持有真实主题类的引用。
  • 实现:代理类在调用真实主题类的方法前后,可以执行额外的操作(如权限检查、日志记录等)。

适配器模式

  • 结构:适配器模式通常包含一个适配器类、一个目标接口和一个被适配者类。适配器类实现目标接口,并在内部持有被适配者类的引用。
  • 实现:适配器类将目标接口的方法调用转换为被适配者类的方法调用。
代理模式与适配器模式(UML)

3. 使用场景

代理模式

  • 场景:当需要控制对某个对象的访问时,可以使用代理模式。例如:
    • 远程代理:控制对远程对象的访问。
    • 虚拟代理:延迟加载对象,直到真正需要时才创建。
    • 保护代理:控制对敏感对象的访问权限。
    • 日志代理:在方法调用前后记录日志。

适配器模式

  • 场景:当需要将一个类的接口转换为另一个接口,以便与现有代码兼容时,可以使用适配器模式。例如:
    • 旧系统与新系统的接口不兼容。
    • 第三方库的接口与现有代码不匹配。
    • 需要复用已有的类,但其接口不符合需求。

集合多属性排序

假如有一个学生数组,想要按照成绩降序、学号升序排序,如何实现?

在Java中,可以使用Comparator接口来实现集合的多属性排序。以下是一个示例,展示了如何对学生数组按照成绩降序、学号升序进行排序。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.util.Arrays;
import java.util.Comparator;

class Student {
private int id;
private String name;
private double score;

public Student(int id, String name, double score) {
this.id = id;
this.name = name;
this.score = score;
}

public int getId() {
return id;
}

public String getName() {
return name;
}

public double getScore() {
return score;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}

public class Main {
public static void main(String[] args) {
Student[] students = {
new Student(101, "Alice", 85.5),
new Student(102, "Bob", 90.0),
new Student(103, "Charlie", 85.5),
new Student(104, "David", 92.5),
new Student(105, "Eve", 88.0)
};

// 使用Comparator进行多属性排序
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 首先按成绩降序排序
int scoreComparison = Double.compare(s2.getScore(), s1.getScore());
if (scoreComparison != 0) {
return scoreComparison;
}
// 如果成绩相同,按学号升序排序
return Integer.compare(s1.getId(), s2.getId());
}
});

// 输出排序后的学生数组
for (Student student : students) {
System.out.println(student);
}
}
}

代码说明

  1. Student类:定义了一个学生类,包含学号(id)、姓名(name)和成绩(score)三个属性。

  2. Main类:包含主方法,用于创建学生数组并进行排序。

  3. 排序逻辑

    • 使用Arrays.sort方法对学生数组进行排序。
    • 通过Comparator接口实现多属性排序:
      • 首先按成绩降序排序(Double.compare(s2.getScore(), s1.getScore()))。
      • 如果成绩相同,按学号升序排序(Integer.compare(s1.getId(), s2.getId()))。
  4. 输出结果:排序后的学生数组将按照成绩降序、学号升序的顺序输出。

输出示例

1
2
3
4
5
Student{id=104, name='David', score=92.5}
Student{id=102, name='Bob', score=90.0}
Student{id=105, name='Eve', score=88.0}
Student{id=101, name='Alice', score=85.5}
Student{id=103, name='Charlie', score=85.5}

native方法

native方法是Java中的一种特殊方法,它使用native关键字进行声明,表示该方法的实现是由非Java代码(通常是C/C++代码)提供的。native方法允许Java程序调用底层操作系统或其他语言编写的库,从而实现Java与本地代码的交互。

volatilesynchronized 的区别

volatilesynchronized 是 Java 中用于处理并发问题的关键字,但它们的作用和使用场景有所不同。

1. volatile

作用:

  • 保证可见性: 当一个线程修改了 volatile 变量的值时,其他线程能够立即看到这个修改。
  • 禁止指令重排序: volatile 变量的读写操作不会被 JVM 优化重排序,从而保证有序性。

适用场景:

  • 单个变量的读写: volatile 适用于单个变量的读写操作,特别是当这个变量被多个线程共享时。
  • 状态标志: 例如,用于控制线程是否继续运行的标志变量。

代码示例:

1
2
3
4
5
6
7
8
9
private volatile boolean flag = false;

public void setFlag(boolean value) {
flag = value;
}

public boolean getFlag() {
return flag;
}

2. synchronized

作用:

  • 互斥访问: synchronized 关键字用于实现互斥访问,确保同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法。
  • 保证可见性: 进入 synchronized 代码块或方法时,会清空工作内存中的变量副本,从主内存中重新加载;退出时,会将工作内存中的变量值刷新到主内存。

适用场景:

  • 多个变量的读写: synchronized 适用于多个变量的读写操作,特别是当这些变量需要保持一致性时。
  • 复杂的同步逻辑: 例如,需要对多个操作进行同步的场景。

代码示例:

1
2
3
4
5
6
7
8
9
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized int getCount() {
return count;
}

区别总结

特性 volatile synchronized
作用 保证可见性、禁止指令重排序 互斥访问、保证可见性
适用场景 单个变量的读写、状态标志 多个变量的读写、复杂的同步逻辑
性能 相对较高,因为不需要加锁 相对较低,因为需要加锁
使用范围 仅限于变量 可以用于方法、代码块
原子性 不能保证复合操作的原子性 可以保证复合操作的原子性