Java 基础
主要特性
- 跨平台: java语言时跨平台的,但是jvm是平台相关的,正是因为jvm的平台相关性,才有了java语言跨平台的基础。先由前端编译器例如(javac)将.java翻译为.class,再由后端编译器(jvm)翻译为对应平台的机器语言,实现一次编译到处运行。
- 面向对象:继承、封装、多态三大基本特征。单一职责、依赖倒置、开放封闭、接口隔离、里氏替换五大基本原则。
- 自动垃圾回收:标记-清除,标记-复制, 标记-整理
堆和栈
- 堆是线程共享的,栈是线程私有的
- 堆空间不足报 OutofMemoryError, 超过栈的最大深度报 StackOverflowError
- 在一个方法中的局部变量,对于基本类型栈上存放的值,对于封装类型栈上存放的对象的引用(即: 在堆中的内存地址), 如下示例所示:
变量 age 是存放在栈内存中,变量 name 在栈内存中存放的是 "lgfei" 这个字符串在堆内存或者字符串常量池的内存地址。class A { void hello(){ String name = "lgfei"; System.out.println(name); int age = 18; System.out.println(age); } }
对象分配内存的过程
- TLAB: Thread Local Allocation Buffer, 是 JVM 为每个线程在堆内存(特别是新生代 Eden 区)预留的一小块内存区域, 避免多线程分配对象时频繁加锁,提升对象分配效率。
多线程
synchronized与volatile
- synchronize是一把锁,可以用在方法或者代码块,它能阻止多个线程同时进入被锁住的那段代码,以此来保证线程安全。
- volatile像是一个人拿着手机在直播,一般用在成员变量,它能把变量实时的值对所有线程可见,以确保每次都能拿到最新的值。同时也可以防止恶意剪辑,颠倒事情发生的顺序(专业术语叫:防止CPU为提高性能造成的指令重排)
最经典的示例代码是单例模式:
如上代码,如果 singleton 不用 volatile 修饰,那么在多个线程同时调用 Singleton.newInstance() 的时候有可能出现 NPE。原因是 singleton = new Sigleton() 不是一个原子操作,它大致可以分为三个步骤:1、分配内存块,2、在内存块上进行数据初始化,3、将内存块的地址指向 singleton 变量。因为CPU指令重排的影响,这三个动作的顺序有可能变成 1->3->2,所有有可能在没有初始化完成的时候 singleton == null 已经返回 false 了,但是实际还没初始化完成,此时就有可能发生 NPE。class Singleton{ private volatile static Singleton sigleton; private Singleton(){} public static newInstance(){ if(singleton == null){ synchronize(Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
CompletableFuture
CompletableFuture 是 java 8 引入的新特性,它使得异步编程和多任务组合编排变得更容易。
get 和 join 的区别
相同点
- 都会阻塞线程等待 future 返回结果
- 如果计算被取消抛出异常:CancellationException
不同点
- get 需要显示的处理异常
- get(long timeout, TimeUnit unit) 方法可设置任务阻塞超时时间
- allOf 和 anyOf
- allOf:所有任务都完成后才返回,因为每个任务返回结果的类型可能不同,所以只能用 CompletableFuture
接收。 那如何获取所有任务的返回值呢?
CompletableFuture<Void> allFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); CompletableFuture<List<Object>> resultFutures = allFuture.thenApply(v -> { return futures.stream().map(f -> f.join()).collect(Colletors.toList()); });
- anyOf:任意一个任务完成后即返回(一般是执行最快的那个),其他则丢弃,并返回 CompletableFuture
- allOf:所有任务都完成后才返回,因为每个任务返回结果的类型可能不同,所以只能用 CompletableFuture
序列化、反序列化
Serializable 接口作用
Serializable 接口用于标识一个类的对象可以被序列化和反序列化。序列化是将对象转换为字节流,以便在网络上传输或保存到文件中。反序列化是将字节流恢复为对象。序列化的类中所有的属性也必须是可序列化的,或者可以使用 transient 关键字来标识某个字段不参与序列化。
serialVersionUID 作用
serialVersionUID 是 Java 中用于标识类版本的一个唯一标识符,用在序列化和反序列化过程中。它的主要作用是确保序列化对象在反序列化时与类定义匹配,以防止反序列化过程中发生 InvalidClassException 异常。
如果不显示指定 serialVersionUID ,会自动生成一个值,无论改动了什么,重新编译后会生成一个新的值,此时会导致反序列化失败。
例如:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and Setters
}
此时我序列化Person对象,并保存到文件中,然后修改Person类
public class Person {
private String name;
private int age;
private String address;
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
// Getters and Setters
}
添加 address 字段,再将原来保存的文件反序列化,就会报 InvalidClassException 异常,因为反序列化时,Person类的serialVersionUID与序列化时生成的值不一致。所以为了能兼容这种情况,我们必须手动指定 serialVersionUID。
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String address;
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
// Getters and Setters
}
为什么在 SpringBoot 中不实现 Serializable 接口也不会报错?
因为 Spring 默认使用的是 JSON 序列化 (Jackson),而不依赖 Serializable 接口实现序列化和反序列化。而像 RMI 远程调用则需要实现 Serializable 接口。
为什么 Dubbo 默认不是 java 原生序列化方式,也需要实现 Serializable 接口?
Dubbo 默认的序列化方式不是 Java 原生的序列化方式,而是使用 Hessian 序列化方式。
在 Hessian 序列化方式中,对象必须实现 Serializable 接口,否则会抛出异常。
cause: org.apache.dubbo.common.serialize.SerializationException: com.alibaba.fastjson2.JSONException: not support none serializable xxx
原因是 dubbo 默认开启了 Serializable 接口检查机制,可以通过 dubbo.application.check-serializable=false 属性关闭。