对于final、finally和finalize不一样的理解
目录
1、final
1.1、不可变性(Immutability)
1.2、内存可见性(Visibility)
1.3、初始化安全(Initialization Safety)
1.4、禁止重排序(Reordering)
1、静态常量
2、实例常量
1.5、与不可变对象的结合
1.6、final 对象引用的内部状态
1.7、适用场景
2、finally
2.1、介绍
1. 资源释放
2. 日志记录或清理操作
2.2、执行顺序
2. 避免在 finally 中使用 return
2.3、注意事项
1. 不会执行的情况
3、finalize
3.1、定义
1. 作用
2. 调用时机
3.2、问题与缺陷
3.3、解决方案
关于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 {@Overrideprotected 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);// 模拟 GCSystem.gc();// 检查队列中是否有被回收的对象if (ref.isEnqueued()) {System.out.println("对象已被回收");}}
}
总结