目录
1.3、初始化安全(Initialization Safety)
关于finally和finalize的介绍,可参考:对Java 资源管理和引用体系的介绍-CSDN博客
1、final
在 Java 中,使用
final
关键字修饰成员变量,尤其是不可变对象可以 保证线程安全,其核心原因在于final
通过以下机制确保了变量的可见性和初始化顺序。
1.1、不可变性(Immutability)
final 变量一旦初始化后不可修改,如果一个成员变量被声明为
final
,则它 只能在构造函数中赋值一次,之后无法被修改。
示例:
public class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value; // 初始化
}
}
- 线程安全的关键:
多个线程读取该变量时,无需担心其他线程修改其值,因此 无需加锁 或其他同步机制。
1.2、内存可见性(Visibility)
当一个类被构造时,如果某个字段被声明为 final
,JVM 会确保:
- 所有对
final
字段的写入必须在构造函数结束前完成。 - 构造函数结束时,JVM 会插入一个 写屏障(Write Barrier),确保这些
final
字段的值被刷新到主内存中。
1、final变量
Java 内存模型(JMM)规定,对 final 变量的写入操作会自动刷新到主内存,通过内存屏障(Memory Barrier)强制刷新缓存,确保其他线程读取时能获取最新值。
2、非 final 变量
非 final 变量可能因为线程缓存(如 CPU 缓存)导致多个线程看到不一致的值。
JVM 在处理 final
字段时会自动插入以下类型的内存屏障:
1.3、初始化安全(Initialization Safety)
对 final 变量的写入会在对象引用赋值给其他线程之前完成。
当一个对象被构造完成后,其他线程访问该对象的 final 成员变量时,可以确保它们已经初始化完成,且不会看到“半初始化”的状态。
示例:
public class SafeInit {
private final int data;
public SafeInit(int data) {
this.data = data; // 构造函数中初始化 final 变量
}
}
// 线程 A 创建对象
SafeInit obj = new SafeInit(42);
// 线程 B 访问 obj.data
System.out.println(obj.data); // 保证看到 42
-
对比非 final 变量:
如果变量未被声明为
final
,构造函数中的写入可能被重排序(Reordering),导致其他线程看到未初始化的值。
1.4、禁止重排序(Reordering)
final 变量的写入具有 happens-before 语义:
构造函数中对
final
变量的写入操作 在对象引用对外可见之前完成。其他线程访问该对象的final
变量时,会看到构造函数中设置的值,而不是默认值(如0
或null
)。
1、
静态常量
static final
变量特点:
可以在声明时直接赋值,也可以通过 static
代码块赋值。在类加载时初始化一次。属于类级别,不是某个实例的属性。
public class Example {
// 声明时直接赋值
private static final int A = 1;
// 通过 static 代码块赋值
private static final int B;
static {
B = 2;
}
}
- 结论:
static final
变量不需要构造函数初始化,因为它们与类的实例无关。
2、实例常量
普通 final
变量特点:属于实例级别,每个对象都有自己的值。
必须在以下位置之一初始化:
1、声明时直接赋值。
2、在构造函数中赋值。
3、在实例初始化块中赋值。
public class Example {
// 声明时直接赋值
private final int a = 1;
// 实例变量
private final int b;
// 构造函数赋值
public Example() {
b = 2;
}
// 实例初始化块赋值
private final int c;
{
c = 3;
}
}
结论:
普通 final
变量必须确保在对象创建时完成初始化,因此必须通过构造函数、声明或实例初始化块实现。
1.5、与不可变对象的结合
如果
final
变量引用的是一个不可变对象(如String
、Integer
),则整个对象的状态在构造完成后不会改变,进一步增强了线程安全性。
示例:
public class Config {
private final String host;
private final int port;
public Config(String host, int port) {
this.host = host;
this.port = port;
}
}
- 线程安全的原因:
所有线程共享的Config
对象的状态是固定的,无需担心并发修改。
1.6、final 对象引用的内部状态
final 只保证引用不可变,不保证对象内部状态不可变。
如果 final
变量引用的是一个可变对象(如 List
、Map
),虽然引用不可变,但对象内部的状态仍可能被修改。
示例:
public class MutableRef {
private final List<String> list = new ArrayList<>();
public void add(String item) {
list.add(item); // 允许修改内部状态
}
}
多个线程调用 add()
方法时,会出现线程安全问题,此时仍需通过同步机制(如 synchronized
或 ConcurrentHashMap
)保证线程安全。
1.7、适用场景
- 常量配置:如数据库连接字符串、系统参数等。
- 不可变对象:如
String
、LocalDateTime
等。 - 线程安全工具类:如缓存键、状态标识符等。
注意事项
- final 不能替代同步:如果变量引用的是可变对象,仍需通过同步机制保护内部状态。
- final 与 volatile 的区别:
final
保证初始化安全和不可变性。volatile
保证变量的可见性和有序性,但允许修改。
通过合理使用 final
,可以显著简化多线程代码的设计,减少竞态条件和内存一致性错误的风险。
2、finally
了解更详细的finally的使用,可参考:合理管控Java语言的异常-CSDN博客
2.1、介绍
finally 是异常处理机制的一部分,用于定义 无论是否发生异常都需要执行的代码块。它通常与 try 和 catch 配合使用,确保资源释放或关键操作在程序流程中始终执行。
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理逻辑
} finally {
// 无论是否发生异常,都会执行的代码
}
1. 资源释放
finally 常用于释放文件、网络连接、数据库连接等资源,确保资源不被泄漏。
示例:关闭文件流
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 读取文件...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close(); // 确保关闭文件流
} catch (IOException e) {
e.printStackTrace();
}
}
}
⚠️ 从 Java 7 开始,推荐使用 try-with-resources 语法自动管理资源,无需显式写 finally:
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 自动关闭 fis
} catch (IOException e) {
e.printStackTrace();
}
2. 日志记录或清理操作
例如,记录操作日志、清理临时文件等。
2.2、执行顺序
1. 无论是否抛出异常,finally 块都会执行:
即使 try 或 catch 块中有 return、 break或 continue,finally 仍会执行(除非程序提前终止)。
2. finally 的执行时机:
或 catch 块执行完毕后,finally 会立即执行。
如果 try 或 catch 中有 return,finally 会在 return 之前执行。
示例:
public class FinallyReturnExample {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
System.out.println("try 块");
return 1;
} catch (Exception e) {
System.out.println("catch 块");
return 2;
} finally {
System.out.println("finally 块");
}
}
}
输出:
try 块
finally 块
1
注意:finally 中的代码会在 return 之前执行,但 不会改变返回值。如果 finally 中也有 return,则会覆盖之前的返回值(需谨慎使用)。
2. 避免在 finally 中使用 return
如果 finally 中有 return,会覆盖 try 或 catch 中的返回值,导致逻辑混乱。
public static int test() {
try {
return 1;
} finally {
return 2; // 覆盖前面的 return
}
}
2.3、注意事项
1. 不会执行的情况
如果 try 或 catch 中调用了 System.exit(0)
,程序会终止,finally 不会执行。
如果程序因 JVM
异常崩溃(如 OutOfMemoryError
),finally 也不会执行。
如果 try 或 catch 中存在 无限循环(如 while (true) {}
),程序会卡死在循环中,finally 不会执行。
总结:
注意:finally 中的代码会在 return 之前执行,但 不会改变返回值。如果 finally 中也有 return,则会覆盖之前的返回值(需谨慎使用)。
如果在try和catch里面分别有return,则会先打印输出的时候,finally在try和catch的return之前打印。
3、finalize
⚠️ finalize
()
在 Java 9 后已被弃用(deprecated),不建议依赖其进行资源清理。
3.1、定义
finalize
()
是Object
类中的一个方法,用于在对象被 垃圾回收(GC) 之前执行一些清理操作(如释放资源)。从 Java 9 开始,finalize()
被标记为@Deprecated
(废弃),不再推荐使用。
通常记录对象被回收的时机或状态。
public class Resource {
@Override
protected void finalize() throws Throwable {
try {
// 释放资源
System.out.println("资源已释放");
} finally {
super.finalize();
}
}
}
1. 作用
- 在对象被 GC 回收之前,JVM 会自动调用该对象的 finalize
()
方法。 - 通常用于释放非内存资源(如文件句柄、网络连接等)。
2. 调用时机
- 对象不再被任何强引用指向时(即变为不可达对象),GC 会将其标记为可回收。
- GC 在回收对象前,可能调用其 finalize
()
方法(具体时机由 JVM 决定,不确定)。
3.2、问题与缺陷
1. 不确定性
GC 时间不可控:
JVM 何时触发 GC 是不确定的,finalize
()
的执行时机也无法预测。可能永远不会执行:
2. 性能问题
GC 负担加重:
每个需要调用
finalize()
的对象会被放入 finalize 队列,由低优先级的守护线程处理,导致 GC 效率降低。可能导致资源延迟释放,甚至引发内存泄漏。
3. 不可靠性
异常未处理:
finalize
()
中抛出异常可能导致整个 GC 过程失败,且没有恢复机制。 多个对象的 finalize()
执行顺序无法保证。
3.3、解决方案
关于cleaner和PhantomReference可参考:对Java 资源管理和引用体系的介绍-CSDN博客
1.1、使用try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
}
2、Cleaner接口
java9开始。
import java.lang.ref.Cleaner;
public class Resource {
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
cleanable = CLEANER.register(this, () -> {
// 清理逻辑
System.out.println("资源已清理");
});
}
}
3、PhantomReference
+ ReferenceQueue
- 通过虚引用配合引用队列,实现资源回收的回调。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
public static void main(String[] args) {
ReferenceQueue<Resource> queue = new ReferenceQueue<>();
PhantomReference<Resource> ref = new PhantomReference<>(new Resource(), queue);
// 模拟 GC
System.gc();
// 检查队列中是否有被回收的对象
if (ref.isEnqueued()) {
System.out.println("对象已被回收");
}
}
}
总结