Java并发与序列化:最佳实践指南
立即解锁
发布时间: 2025-08-18 00:25:51 阅读量: 1 订阅数: 4 

### Java并发与序列化:最佳实践指南
#### 1. 谨慎使用延迟初始化
延迟初始化是指将字段的初始化延迟到需要其值时进行。如果永远不需要该值,则该字段永远不会被初始化。此技术适用于静态字段和实例字段。虽然延迟初始化主要是一种优化手段,但它也可用于打破类和实例初始化中的有害循环。
不过,和大多数优化一样,对于延迟初始化,最好的建议是“除非必要,否则不要使用”。它是一把双刃剑,虽然降低了类初始化或实例创建的成本,但增加了访问延迟初始化字段的成本。是否采用延迟初始化,取决于延迟初始化字段最终需要初始化的比例、初始化的成本以及每个字段的访问频率。
以下是几种初始化的方式:
- **正常初始化**:对于大多数情况,正常初始化优于延迟初始化。示例代码如下:
```java
// Normal initialization of an instance field
private final FieldType field = computeFieldValue();
```
- **使用同步访问器进行延迟初始化**:如果使用延迟初始化来打破初始化循环,可以使用同步访问器,这是最简单、最清晰的方法。示例代码如下:
```java
// Lazy initialization of instance field - synchronized accessor
private FieldType field;
synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
```
- **静态字段的延迟初始化持有者类惯用法**:如果需要对静态字段进行延迟初始化以提高性能,可以使用延迟初始化持有者类惯用法。示例代码如下:
```java
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }
```
- **实例字段的双重检查惯用法**:如果需要对实例字段进行延迟初始化以提高性能,可以使用双重检查惯用法。示例代码如下:
```java
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) {
// First check (no locking)
synchronized(this) {
result = field;
if (result == null)
// Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
```
- **单检查惯用法**:对于可以容忍重复初始化的实例字段,可以使用单检查惯用法。示例代码如下:
```java
// Single-check idiom - can cause repeated initialization!
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
```
- **竞态单检查惯用法**:如果不关心每个线程是否重新计算字段的值,并且字段类型是除 long 或 double 之外的基本类型,可以考虑使用竞态单检查惯用法。
#### 2. 不要依赖线程调度器
当有多个线程可运行时,线程调度器决定哪些线程可以运行以及运行多长时间。任何合理的操作系统都会尽量公平地做出这个决定,但策略可能会有所不同。因此,编写良好的程序不应依赖于这种策略的细节。依赖线程调度器来保证正确性或性能的程序可能不具有可移植性。
编写健壮、响应迅速且可移植的程序的最佳方法是确保可运行线程的平均数量不会显著超过处理器的数量。主要技术是让每个线程完成一些有用的工作,然后等待更多任务。线程在没有做有用工作时不应运行。
以下是一个错误示例,展示了忙等待的不良影响:
```java
// Awful CountDownLatch implementation - busy-waits incessantly!
public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException(count + " < 0");
this.count = count;
}
public void await() {
while (true) {
synchronized(this) {
if (count == 0) return;
}
}
}
public synchronized void countDown() {
if (count != 0)
count--;
}
}
```
当 1000 个线程等待一个闩锁时,`SlowCountDownLatch` 比 `CountDownLatch` 慢约 2000 倍。
当遇到由于某些线程相对于其他线程获得的 CPU 时间不足而几乎无法工作的程序时,不要试图通过调用 `Thread.yield` 来“修复”程序。更好的做法是重构应用程序,以减少并发可运行线程的数量。同样,调整线程优先级也是不可取的,它是 Java 平台中可移植性最差的特性之一。
对于并发测试,建议使用 `Thread.sleep(1)` 而不是 `Thread.yield`。
#### 3. 避免使用线程组
线程组最初被设想为一种出于安全目的隔离小程序的机制,但它并没有真正实现这一承诺,其安全重要性已经降低,在 Java 安全模型的标准著作中甚至都没有提及。
线程组提供的功能很少,它允许将某些线程原语一次性应用于一组线程,但其中一些原语已被弃用,其余的也很少使用。而且,从线程安全的角度来看,`ThreadGroup` API 很薄弱。例如,要获取线程组中活动线程的列表,必须调用 `enumerate` 方法,但无法保证 `activeCount` 方法返回的计数在分配数组并传递给 `enumerate` 方法时仍然准确。
在 Java 1.5 之前,`ThreadGroup.uncaughtException` 方法是在线程抛出未捕获异常时获得控制权的唯一方法,但从 Java 1.5 开始,`Thread` 的 `setUncaughtExceptionHandler` 方法提供了相同的功能。
因此,线程组最好被视为一次不成功的实验,应该忽略它们的存在。如果设计一个处理逻辑线程组的类,应该使用线程池执行器。
#### 4. 谨慎实现 Serializable 接口
允许类的实例进行序列化,只需在类的声明中添加 `implements Serializable` 即可。但这其实是一种误解,实现 `Serializable` 接口虽然看似简单,但长期成本往往很高。
实现 `Serializable` 接口的成本主要体现在以下几个方面:
- **降低类实现的灵活性**:类实现 `Serializable` 后,其字节流编码成为导出 API 的一部分,一旦广泛分发,通常需要永远支持该序列化形式。如果接受
0
0
复制全文
相关推荐










