《Effective Java》核心思想概览
《Effective Java》由 Joshua Bloch 编写,是 Java 开发领域的“圣经”,它并非一本入门教程,而是一本专注于编写高质量、健壮、可维护、高效 Java 代码的实践指南,其核心思想可以概括为:

关注正确性、健壮性、可使用性和性能。
全书共 11 章,90 条“经验法则”(Item),下面我将它们分为几个大的模块进行梳理。
第一部分:创建与销毁对象
这是 Java 编程的基础,也是最容易出错的地方。
考虑用静态工厂方法代替构造器
- 目的:提供一种创建对象的替代方式。
- 优点:
- 有名称:可以更清晰地表达创建的对象的含义(如
BigInteger.probablePrime())。 - 不必在每次调用时都创建新对象:可以返回预创建的实例(享元模式),或缓存实例(如
Boolean.valueOf())。 - 可以返回原返回类型的任何子类对象:增加了灵活性(如
Collections.emptyList()返回的是ImmutableList)。 - 在创建参数化类型实例时,代码更简洁(Java 7+ 的菱形操作符
<>解决了大部分问题)。
- 有名称:可以更清晰地表达创建的对象的含义(如
- 缺点:
- 公有或受保护的类没有公有或受保护的构造器时,无法被继承。
- 与普通的工厂方法混淆,需要良好的文档或命名规范(如
of(),valueOf(),instance(),create())。
- 最佳实践:提供一个公有构造器和一个静态工厂方法,两者并存。
遇到多个构造器参数时,考虑使用构建器
- 问题:当类有很多可选参数时,传统的“重叠构造器”(Telescoping Constructor)模式难以阅读和编写。
- 解决方案:Builder 模式。
- 创建一个
Builder内部类,为每个可选参数设置一个setter方法,并返回Builder实例以支持链式调用。 Builder类提供一个build()方法,用于最终构造和验证目标对象。
- 创建一个
- 优点:
- 代码可读性强:
Person.builder().name("张三").age(30).build()。 - 参数可变,易于扩展。
- 可以在
build()方法中执行参数校验。
- 代码可读性强:
- 适用场景:特别是当对象有很多可选参数时,对于简单的对象,直接使用构造器或静态工厂方法更简单。
用私有构造器或枚举类型强化 Singleton 属性
- 目的:确保一个类只有一个实例,并提供一个全局访问点。
- 最佳实践:
- 优先使用枚举类型:
public enum Elvis { INSTANCE; ... },这是最简洁、线程安全,并能防止反射攻击和序列化问题的实现方式。 - 传统方式:私有构造器 + 一个公有静态 final 实例,需要处理序列化问题(实现
readResolve()方法以防止创建新实例)。
- 优先使用枚举类型:
通过私有构造器强化不可变类
- 不可变类的特征:
- 所有字段都是
final的。 - 对象在创建后,其状态不能被修改。
- 不提供任何“setter”方法。
- 所有可变组件(如
Date、List)的引用都是防御性拷贝的。
- 所有字段都是
- 优点:
- 天生线程安全:无需同步。
- 简单可靠:状态不变,易于理解和使用。
- 可作为 Map 的键、Set 的元素。
- 强化不可变性:将类声明为
final,防止子类覆盖方法并改变状态,构造器设为private,防止外部创建实例(通常通过静态工厂方法提供实例)。
优先依赖注入,而非硬编码资源
- 问题:在类内部创建或查找依赖的资源(如数据库连接、服务),会导致代码耦合度高、难以测试。
- 解决方案:依赖注入,将依赖的资源作为参数传递给构造器或静态工厂方法。
- 优点:
- 灵活性高:可以在运行时替换不同的依赖实现。
- 可测试性强:可以轻松注入“mock”对象进行单元测试。
- 重用性高。
避免创建不必要的对象
- 核心思想:复用不可变对象,由于它们的状态不可变,所以是绝对安全的。
- 反例:在循环中创建不必要的对象。
// 错误:每次循环都创建一个新的 String 实例 for (int i = 0; i < N; i++) { String s = new String("stringette"); } // 正确:复用同一个 String 实例 String s = "stringette"; for (int i = 0; i < N; i++) { // ... } - 注意:
- 不要为了性能而牺牲代码的清晰性。
- 优先使用基本类型而不是其包装类型(如
int而非Integer),避免不必要的自动装箱。 - 对象池通常弊大于利(如
StringBuffer在早期版本中有用,但现在StringBuilder更好)。
消除过期的对象引用
- 问题:无意中保留了对不再需要的对象的引用,导致 GC 无法回收,引发内存泄漏。
- 常见场景:
- 缓存:缓存没有被清理机制,导致旧数据堆积。
- 监听器/回调:没有取消注册,导致回调对象一直被持有。
- 解决方案:
- 将引用设为
null:适用于长生命周期的局部变量。 - 使用
WeakHashMap:当键不再被任何强引用指向时,条目会自动被 GC。 - 对于缓存:使用
WeakHashMap或实现一个定时清理机制。 - 对于监听器:提供一个显式的
removeListener方法。
- 将引用设为
避免使用 Finalizer 和 Cleaner
- Finalizer(终结器)是 Java 1.0 的产物,存在严重问题:
- 不可预测性:执行时机完全不可控,甚至可能不执行。
- 性能差:会显著拖慢 GC 速度。
- 危险:在 Finalizer 中抛出的异常会被忽略,可能导致对象处于不一致状态。
- Cleaner(Java 9 引入,替代了
PhantomReference+Finalizer)比 Finalizer 稍好,但仍不推荐。 - 替代方案:
try-finally块:用于释放资源(如文件、数据库连接)。AutoCloseable接口:配合try-with-resources语句,实现资源的自动、安全释放,这是目前最佳实践。
第二部分:通用程序设计
这部分关注日常编码的最佳实践。

覆盖 equals 时必须覆盖 hashCode
- 契约:
a.equals(b)为true,a.hashCode()必须等于b.hashCode()。 - 违反后果:无法在基于哈希的集合(如
HashMap,HashSet)中正常工作,对象会被“丢失”。 - 实现步骤:
- 存储
hashCode的一个局部变量result,初始化为非零值。 - 对每个“重要”字段
f(参与equals比较的字段):- 布尔值:
c ? 1 : 0 byte,char,short,int:(int) flong:int)(f ^ (f >>> 32))float:Float.floatToIntBits(f)double:Double.doubleToLongBits(f),然后按long处理。- 引用对象:递归调用其
hashCode(),如果为null,则指定一个常量(如0)。 - 数组:对每个元素调用上述规则,或使用
Arrays.hashCode()。
- 布尔值:
- 合并结果:
result = 31 * result + c;(31是一个质数,能有效减少哈希冲突)。 - 返回
result。
- 存储
- 捷径:使用 IDE 生成,或使用
Objects.hash()方法(性能稍差,但简洁)。
始终要覆盖 toString
- 目的:提供一个清晰、信息丰富的字符串表示,便于调试和日志记录。
- 约定:返回一个格式良好的、易于阅读的字符串,通常包含对象的关键信息。
- 反例:直接继承的
toString返回的是类名加 加哈希码,没有信息量。
谨慎地覆盖 clone
Cloneable接口:设计存在缺陷,它不是一个“可克隆”的标记,而是启动了一个“克隆机制”的接口。- 问题:
- 没有
final方法:子类可以破坏克隆的约定。 - 浅拷贝:
Object.clone()默认实现是浅拷贝,对于包含可变对象的类来说是不安全的。
- 没有
- 替代方案:
- 提供拷贝构造器:
MyClass(MyClass other)。 - 提供拷贝工厂方法:
MyClass copyOf(MyClass other)。 - 如果确实需要实现
Cloneable:- 在
clone()方法中调用super.clone()。 - 对所有可变字段进行防御性拷贝。
- 将
clone()方法声明为protected。
- 在
- 提供拷贝构造器:
考虑实现 Comparable 接口
- 目的:让对象具有“自然排序”的能力。
- 优点:一旦实现,对象就可以被
Collections.sort()、Arrays.sort()等方法排序,也可以作为SortedSet、SortedMap的元素。 - 约定:必须确保
sgn(a.compareTo(b)) == -sgn(b.compareTo(a)),且a.compareTo(b) > 0 && b.compareTo(c) > 0意味着a.compareTo(c) > 0。 - 实现技巧:使用基本类型的比较器(如
Integer.compare,Double.compare)或Comparator.comparing()方法链。
第三部分:类与接口
这部分关注类和接口的设计原则。
使类和成员的可访问性最小化
- 核心原则:封装。
- 访问级别优先级:
private:仅在本类中可见。package-private(无修饰符):在同一包内可见,这是最常用的访问级别。protected:子类和同一包内可见。public:任何地方可见。
- 实践:
- 顶层的类和接口要么是
public,要么是package-private。 - 嵌套类(
private static)可以是public,但通常不推荐。 - 实例字段绝不能是
public(除了常量)。public static final的字段必须是基本类型或不可变对象的引用。
- 顶层的类和接口要么是
在公有类中使用访问方法而非公有字段
- 问题:暴露公有字段会破坏封装,使得类的内部实现无法修改。
- 解决方案:提供
getter和setter方法。 - 优点:
- 未来可以修改内部实现:从
String切换到StringBuilder,而不影响调用方。 - 可以添加约束逻辑:在
setter中进行参数校验。 - 可以维护不变性:确保对象状态始终有效。
- 未来可以修改内部实现:从
- 例外:对于不可变的、
public static final的字段,可以直接暴露。
使可变性最小化
- 再次强调不可变类的优点:线程安全、简单、可靠。
- 如果类必须是可变的:
- 不要提供任何会修改对象状态的方法。
- 确保所有字段都是
final的。 - 确保所有可变组件的引用都是防御性拷贝的。
- 不要提供“setter”方法。
复合优先于继承
- 继承的问题:
- 违反封装:子类依赖于父类的具体实现,父类的任何改动都可能破坏子类。
- 脆弱的基类问题:父类的更新可能导致所有子类出错。
- 解决方案:组合/复合模式。
- 在新类中持有旧类的实例(
private final)。 - 新类的方法通过委托给旧类的实例来实现。
- 这更灵活、更健壮。
- 在新类中持有旧类的实例(
- 继承的适用场景:
- “is-a”关系:子类真正是父类的一种。
- 需要重写父类方法。
- 与框架设计相关,如
Activity继承Context。
要么为继承而设计,并提供文档说明,要么就禁止继承
- 如果一个类不是被设计为可继承的,就应该禁止继承(声明为
final或所有构造器为private)。 - 如果要为继承而设计,必须:
- 文档说明:详细说明每个方法的可覆盖行为,特别是构造器。
- 保护内部状态:使用
protected访问级别暴露必要的内部状态。 - 小心构造器:不要调用任何可覆盖的方法(
private final方法除外),因为子类可能还没有初始化。 - 考虑实现
Serializable的复杂问题。
接口优于抽象类
- 优点:
- 灵活:一个类可以实现多个接口。
- 干净:接口定义类型,可以混合搭配。
- 现代:Java 8+ 接口可以包含
default和static方法,功能更强大。
- 抽象类的适用场景:
- 需要共享代码(非
default方法)。 - 需要定义非
static、非final的字段。 - 需要访问控制(
protected成员)。
- 需要共享代码(非
接口只用于定义类型
- 问题:接口中不应该包含
static final字段,除非它们是“常量接口”(Constant Interface)。 - 反例:
Math.PI应该通过Math类访问,而不是通过一个Constants接口。 - 解决方案:如果需要一组常量,应该将其放在一个不可变的工具类中。
类层次结构优于标签类
- 标签类:一个类用
int或enum字段来表示其“类型”,然后用if-else分支来处理不同类型的行为。 - 问题:代码臃肿、丑陋、易出错、难以扩展。
- 解决方案:类层次结构,为每种类型创建一个子类,利用多态来消除
if-else分支。
用函数对象表示策略
-
问题:需要将算法(如比较器)作为参数传递。
-
解决方案:策略模式,将算法封装成一个对象。
-
Java 8+ 最佳实践:使用Lambda 表达式或方法引用来创建匿名函数对象,代码极其简洁。
(图片来源网络,侵删)// Java 8 之前 Arrays.sort(strings, new Comparator<String>() { public int compare(String s1, String s2) { return s1.length() - s2.length(); } }); // Java 8 之后 Arrays.sort(strings, (s1, s2) -> s1.length() - s2.length());
优先考虑接口而非反射
- 反射:可以在运行时动态地加载类、获取方法、调用方法。
- 缺点:
- 代码脆弱:依赖于类名、方法名等字符串,容易因代码重构而破坏。
- 性能差:比直接调用慢很多。
- 安全性受限:安全管理器可能限制反射的使用。
- 适用场景:
- 组件框架:如 Spring, Hibernate,需要动态加载用户定义的类。
- 基于注解的代码生成工具。
- 需要处理源代码中不存在的类。
谨慎使用优化
- 原则:不要过早优化。
- William Wulf 的名言:“过早的优化是万恶之源。”
- 实践:
- 不要为了性能而牺牲代码的清晰性和简洁性。
- 在优化之前,先进行性能分析,找到真正的瓶颈。
- 算法的选择比任何微优化都重要得多。
普遍性优于特殊性
- 原则:简单化。
- 实践:
- 优先使用基本数据类型,而不是包装类。
- 优先使用标准库,而不是自己实现。
- 优先使用简单的、众所周知的算法和数据结构,而不是复杂的、炫技的。
- 代码应该像故事一样,易于阅读和理解。
第四部分:泛型
泛型是 Java 的一大特性,能提供编译时类型安全。
列表优先于数组
- 数组:
- 协变:
String[]是Object[]的子类型,运行时类型检查。 - 具体化:在运行时知道元素类型,无法创建泛型数组(如
new List<String>[10]是非法的)。
- 协变:
- 泛型集合:
- 不可变:
List<String>不是List<Object>的子类型,编译时类型检查。 - 类型擦除:在运行时泛型信息被擦除,所有类型参数都被替换为它们的边界(通常是
Object)。
- 不可变:
- 优点:泛型集合更安全、更灵活、更具表达力。
优先在泛型中使用 <?> 而非原始类型
- 原始类型:不使用泛型参数,如
List。 - 问题:会失去编译时类型检查,容易引发
ClassCastException。 - 解决方案:无界通配符
<?>。List<?>可以持有任何List,但你不知道它具体是什么类型,也不能向其中添加任何元素(除了null)。- 它代表的是“一个不知道具体元素类型的
List”,比原始类型List更安全。
- 何时使用
<?>:当方法只需要读取集合中的元素,而不关心其具体类型时。
优先使用泛型方法
- 目的:让方法独立于其声明的类或接口,就能操作泛型类型。
- 语法:在返回类型前放置类型参数列表。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; } - 优点:代码更通用、类型更安全。
利用有限制通配符来增加 API 的灵活性
-
PECS 原则:Producer Extends, Consumer Super。
- 生产者:如果参数化类型只用来生产 T 值(从集合中读取),使用
<? extends T>。List<? extends Number>可以是List<Integer>或List<Double>,你可以安全地从中读取Number。
- 消费者:如果参数化类型只用来消费 T 值(向集合中写入),使用
<? super T>。List<? super Integer>可以是List<Integer>或List<Number>,你可以安全地向其中写入Integer。
- 生产者:如果参数化类型只用来生产 T 值(从集合中读取),使用
-
Comparable/Comparator:<? super T>用于消费者,<? extends T>用于生产者。// List<? extends T> 是 T 的生产者 public static <T> T max(List<? extends T> list) // Comparator<? super T> 是 T 的消费者 public static <T> void sort(List<T> list, Comparator<? super T> c)
优先考虑类型安全的异构容器
-
问题:
Map<Class<?>, Object>可以存储不同类型的对象,但会丢失类型信息,每次取值都需要强制类型转换。 -
解决方案:类型安全的异构容器。
-
使用
Class对象作为键,但利用泛型来确保值的类型与键的类型一致。 -
核心技巧:使用
Class<T>作为键,并将其作为Class<T>的泛型参数。public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public <T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type)); } }
-
第五部分:枚举与注解
用 enum 代替 int 常量
int常量的问题:- 类型不安全:可以传递任何
int值。 - 没有内置的打印功能。
- 不方便扩展行为。
- 类型不安全:可以传递任何
enum的优点:- 类型安全:编译器会检查。
- 提供丰富的行为:可以给枚举常量添加方法、字段。
- 有名字和顺序:可以使用
name()和ordinal()。 - 可以实现任意接口。
用实例域代替序数
-
问题:使用
ordinal()方法来获取枚举常量的序数(0, 1, 2...)并将其用于业务逻辑。 -
缺点:脆弱,如果修改了枚举常量的顺序,业务逻辑就会出错。
-
解决方案:为每个常量关联一个数据域。
public enum Ensemble { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12); private final int numberOfMusicians; Ensemble(int size) { this.numberOfMusicians = size; } public int numberOfMusicians() { return numberOfMusicians; } }
用 EnumSet 代替位域
- 位域:使用
int或long的位来表示一组标志。 - 问题:类型不安全,需要手动处理位运算,API 丑陋。
EnumSet:- 专门为枚举集合设计的高性能
Set实现。 - 内部使用位向量实现,性能媲美位域。
- API 清晰,类型安全。
- 专门为枚举集合设计的高性能
- 最佳实践:如果需要表示一组枚举常量,
EnumSet是完美的选择。
用 EnumMap 代替序数索引
- 问题:将枚举的
ordinal()作为List或数组的索引。 - 缺点:脆弱、浪费空间、效率低下。
EnumMap:- 专门为枚举键设计的高性能
Map实现。 - 内部使用数组实现,访问速度极快。
- API 清晰,类型安全。
- 专门为枚举键设计的高性能
慎用注解
- 注解:为元数据提供了一种形式化的方法,不影响代码的语义。
- 适用场景:
- 框架:如 Spring (
@Autowired), JUnit (@Test)。 - 代码生成工具:如 Lombok, ButterKnife。
- 替代模式:如
@Override。
- 框架:如 Spring (
- 实践:
- 编写自己的注解类型:需要定义
@interface,并使用@Target和@Retention元注解来限定其使用范围和生命周期。 - 使用
@Documented使注解出现在 Javadoc 中。 - 使用
@Repeatable允许在同一声明上多次使用同一个注解(Java 8+)。
- 编写自己的注解类型:需要定义
第六部分:异常
只针对异常的情况才使用异常
- 错误用法:将异常用于正常的控制流。
// 错误:用异常来处理 for 循环的终止条件 try { int i = 0; while (true) { range[i++].doSomething(); } } catch (ArrayIndexOutOfBoundsException e) { // ... } - 正确用法:异常只用于处理那些真正异常、不期望发生的事件。
对可恢复的条件使用受检异常,对编程错误使用运行时异常
- 受检异常:在方法签名中声明(
throws),调用者必须处理(try-catch或throws)。- 适用场景:可预见的、可恢复的错误,如
IOException,SQLException。
- 适用场景:可预见的、可恢复的错误,如
- 运行时异常:无需在方法签名中声明。
- 适用场景:编程错误,通常由
NullPointerException,IllegalArgumentException表示,这些错误本就不应该发生。
- 适用场景:编程错误,通常由
- 错误:
Error的子类,通常表示 JVM 严重的系统错误,应用程序通常无法恢复,也不应该捕获。
避免不必要地使用受检异常
- 问题:过度使用受检异常会使 API 变得臃肿,调用者代码变得冗长。
- 解决方案:
- 返回一个可选值或空对象:如
Optional<T>,或者一个表示“未找到”的默认对象。 - 抛出一个运行时异常:如果条件是程序员的错误(如参数非法)。
- 确保异常有足够的描述信息。
- 返回一个可选值或空对象:如
优先使用标准的异常
- 优点:
- 易于理解:开发人员熟悉这些异常的含义。
- 代码简洁:无需自己定义异常类。
- 常用标准异常:
IllegalArgumentException:参数不合法。IllegalStateException:对象状态不正确(如方法在未初始化时被调用)。NullPointerException:参数为 null 且该参数不允许为 null。IndexOutOfBoundsException:索引越界。ConcurrentModificationException:在迭代过程中集合被并发修改。UnsupportedOperationException:对象不支持某个操作。
抛出与抽象相对应的异常
- 问题:底层异常(如
SQLException)被直接抛给上层调用者,暴露了底层的实现细节。 - 解决方案:异常转译。
- 在更高层次的抽象中,捕获底层的异常,并抛出一个与当前抽象级别更相关的异常。
- 可以将原始异常作为
cause传递给新异常,使用initCause()方法或构造函数。
- 最佳实践:高层方法应该抛出与它的抽象级别一致的异常。
异常链
- 目的:将捕获的低层异常作为新异常的“原因”(cause)。
- 好处:保留了完整的调用栈信息,便于调试。
- 实现:在创建新异常时,将原始异常作为参数传入。
try { // ... } catch (LowLevelException e) { throw new HighLevelException("High-level problem", e); }
方法抛出的所有异常都要有文档
- 目的:让调用者知道可能需要处理哪些异常。
- 实践:使用
@throwsJavadoc 标签。/** * @throws ArithmeticException if overflow occurs */ public static int add(int a, int b) { // ... }
力争异常失败原子性
- 目标:一个失败的方法调用,应该使对象保持在被调用之前的状态。
- 实现方式:
- 设计不可变对象。
- 在执行操作前检查参数的有效性(先检查,后执行)。
- 在对象状态的一部分被修改后,出现异常,执行回滚操作。
- 对于可变对象,在修改前创建临时副本,成功后再替换原对象。
不要忽略异常
- 问题:
catch (Exception e) {}或catch (Exception e) { e.printStackTrace(); }。 - 后果:异常被“吞掉”,程序可能在错误状态下继续运行,难以排查问题。
- 解决方案:
- 处理它:记录日志、向用户提示、进行恢复。
- 传播它:重新抛出异常(可能包装后)。
- 如果确实无法处理,至少要记录一条有意义的日志。
// 正确的做法:记录日志 catch (NoSuchElementException e) { log.warn("Element not found", e); // ... 或者抛出一个更合适的业务异常 }
第七部分:并发
同步访问共享的可变数据
- 核心问题:当多个线程同时读写共享数据时,会导致竞态条件。
- 解决方案:
- 线程封闭:将数据限制在单个线程内访问(如
ThreadLocal)。 - 同步:使用
synchronized关键字或显式锁(java.util.concurrent.locks.Lock)。 - 使用不可变对象。
- 使用线程安全类(如
ConcurrentHashMap,CopyOnWriteArrayList)。
- 线程封闭:将数据限制在单个线程内访问(如
- 原则:除非数据是线程封闭、不变或由线程安全类保护,否则必须在所有访问它的线程中同步。
避免过度同步
- 问题:
- 性能下降:同步会降低并发性。
- 死锁:如果同步块中获取多个锁,且顺序不一致。
- 死锁:如果同步块中获取多个锁,且顺序不一致。
- 活性失败:如线程饥饿、活锁。
- 解决方案:
- 在同步块之外执行尽可能多的工作。
- 不要在同步块中调用外部方法,这些方法可能会获取锁或执行耗时操作。
- 使用
java.util.concurrent包中的高级工具,它们经过精心设计,能更好地处理并发问题。 - 使用并发集合,它们通常比同步的
Collections.synchronizedXxx()性能更好。
优先使用 concurrent 工具而非 synchronized
java.util.concurrent包:提供了更高级、更强大、更灵活的并发工具。- 优势:
- 更高的并发性:如
ConcurrentHashMap使用分段锁或 CAS 操作。 - 更丰富的功能:如
CountDownLatch,Semaphore,CyclicBarrier,ExecutorService。 - 更易于使用:
ExecutorService框架比直接操作线程更简单、更强大。
- 更高的并发性:如
synchronized的适用场景:- 简单的同步需求。
- 当
synchronized的隐式锁已经足够时。
并发工具优先于 wait 和 notify
wait()和notify():- 容易用错,必须与
synchronized配合使用。 - 容易导致死锁和性能问题。
- API 难以理解。
- 容易用错,必须与
java.util.concurrent中的高级工具:CountDownLatch:允许一个或多个线程等待一组事件发生。Semaphore:控制同时访问某个资源的线程数量。CyclicBarrier:让一组线程在到达某个屏障时阻塞,直到所有线程都到达。Exchanger:允许两个线程在某个点交换对象。BlockingQueue:提供了阻塞的put和take操作,是生产者-消费者模式的理想选择。
线程安全性的文档化
- 目的:让其他开发者知道如何正确地使用你的类。
- 实践:
- 明确地说明类及其方法的线程安全级别。
- 线程安全级别:
- 不可变:绝对安全。
- 无状态:没有可变域,绝对安全。
- 线程安全:所有方法都有内部同步,可以并发调用。
- 条件线程安全:除了单个方法外,某些方法的组合需要外部同步。
- 线程兼容:不是线程安全的,但可以安全地由外部同步。
- 线程不安全:绝对不安全。
- 在文档中说明哪些锁需要外部同步。
慎用延迟初始化
- 问题:延迟初始化(在第一次使用时才初始化)在单线程中很简单,但在多线程中很复杂。
- 单线程解决方案:
private FieldType field; public FieldType getField() { if (field == null) { field = new FieldType(); } return field; } - 多线程解决方案:
- 双重检查锁:
volatile+synchronized,Java 5+ 后volatile保证了可见性和禁止指令重排,是可行的。 - 静态内部类模式(推荐):利用类加载机制保证线程安全和延迟初始化。
- 双重检查锁:
