杰瑞科技汇

Effective Java笔记,核心要点有哪些?

《Effective Java》核心思想概览

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

Effective Java笔记,核心要点有哪些?-图1
(图片来源网络,侵删)

关注正确性、健壮性、可使用性和性能。

全书共 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”方法。
    • 所有可变组件(如 DateList)的引用都是防御性拷贝的。
  • 优点
    • 天生线程安全:无需同步。
    • 简单可靠:状态不变,易于理解和使用。
    • 可作为 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 语句,实现资源的自动、安全释放,这是目前最佳实践

第二部分:通用程序设计

这部分关注日常编码的最佳实践。

Effective Java笔记,核心要点有哪些?-图2
(图片来源网络,侵删)

覆盖 equals 时必须覆盖 hashCode

  • 契约a.equals(b)truea.hashCode() 必须等于 b.hashCode()
  • 违反后果:无法在基于哈希的集合(如 HashMap, HashSet)中正常工作,对象会被“丢失”。
  • 实现步骤
    1. 存储 hashCode 的一个局部变量 result,初始化为非零值。
    2. 对每个“重要”字段 f(参与 equals 比较的字段):
      • 布尔值:c ? 1 : 0
      • byte, char, short, int(int) f
      • longint)(f ^ (f >>> 32))
      • floatFloat.floatToIntBits(f)
      • doubleDouble.doubleToLongBits(f),然后按 long 处理。
      • 引用对象:递归调用其 hashCode(),如果为 null,则指定一个常量(如 0)。
      • 数组:对每个元素调用上述规则,或使用 Arrays.hashCode()
    3. 合并结果:result = 31 * result + c;31 是一个质数,能有效减少哈希冲突)。
    4. 返回 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() 等方法排序,也可以作为 SortedSetSortedMap 的元素。
  • 约定:必须确保 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() 方法链。

第三部分:类与接口

这部分关注类和接口的设计原则。

使类和成员的可访问性最小化

  • 核心原则封装
  • 访问级别优先级
    1. private:仅在本类中可见。
    2. package-private (无修饰符):在同一包内可见,这是最常用的访问级别
    3. protected:子类和同一包内可见。
    4. public:任何地方可见。
  • 实践
    • 顶层的类和接口要么是 public,要么是 package-private
    • 嵌套类(private static)可以是 public,但通常不推荐。
    • 实例字段绝不能是 public(除了常量)。public static final 的字段必须是基本类型或不可变对象的引用。

在公有类中使用访问方法而非公有字段

  • 问题:暴露公有字段会破坏封装,使得类的内部实现无法修改。
  • 解决方案:提供 gettersetter 方法。
  • 优点
    • 未来可以修改内部实现:从 String 切换到 StringBuilder,而不影响调用方。
    • 可以添加约束逻辑:在 setter 中进行参数校验。
    • 可以维护不变性:确保对象状态始终有效。
  • 例外:对于不可变的public static final 的字段,可以直接暴露。

使可变性最小化

  • 再次强调不可变类的优点:线程安全、简单、可靠。
  • 如果类必须是可变的
    • 不要提供任何会修改对象状态的方法
    • 确保所有字段都是 final
    • 确保所有可变组件的引用都是防御性拷贝的
    • 不要提供“setter”方法

复合优先于继承

  • 继承的问题
    • 违反封装:子类依赖于父类的具体实现,父类的任何改动都可能破坏子类。
    • 脆弱的基类问题:父类的更新可能导致所有子类出错。
  • 解决方案组合/复合模式
    • 在新类中持有旧类的实例(private final)。
    • 新类的方法通过委托给旧类的实例来实现。
    • 这更灵活、更健壮。
  • 继承的适用场景
    • “is-a”关系:子类真正是父类的一种。
    • 需要重写父类方法
    • 与框架设计相关,如 Activity 继承 Context

要么为继承而设计,并提供文档说明,要么就禁止继承

  • 如果一个类不是被设计为可继承的,就应该禁止继承(声明为 final 或所有构造器为 private)。
  • 如果要为继承而设计,必须:
    • 文档说明:详细说明每个方法的可覆盖行为,特别是构造器。
    • 保护内部状态:使用 protected 访问级别暴露必要的内部状态。
    • 小心构造器:不要调用任何可覆盖的方法(private final 方法除外),因为子类可能还没有初始化。
    • 考虑实现 Serializable 的复杂问题。

接口优于抽象类

  • 优点
    • 灵活:一个类可以实现多个接口。
    • 干净:接口定义类型,可以混合搭配。
    • 现代:Java 8+ 接口可以包含 defaultstatic 方法,功能更强大。
  • 抽象类的适用场景
    • 需要共享代码(非 default 方法)。
    • 需要定义非 static、非 final 的字段。
    • 需要访问控制(protected 成员)。

接口只用于定义类型

  • 问题:接口中不应该包含 static final 字段,除非它们是“常量接口”(Constant Interface)。
  • 反例Math.PI 应该通过 Math 类访问,而不是通过一个 Constants 接口。
  • 解决方案:如果需要一组常量,应该将其放在一个不可变的工具类中。

类层次结构优于标签类

  • 标签类:一个类用 intenum 字段来表示其“类型”,然后用 if-else 分支来处理不同类型的行为。
  • 问题:代码臃肿、丑陋、易出错、难以扩展。
  • 解决方案类层次结构,为每种类型创建一个子类,利用多态来消除 if-else 分支。

用函数对象表示策略

  • 问题:需要将算法(如比较器)作为参数传递。

  • 解决方案策略模式,将算法封装成一个对象。

  • Java 8+ 最佳实践:使用Lambda 表达式方法引用来创建匿名函数对象,代码极其简洁。

    Effective Java笔记,核心要点有哪些?-图3
    (图片来源网络,侵删)
    // 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 的名言:“过早的优化是万恶之源。
  • 实践
    1. 不要为了性能而牺牲代码的清晰性和简洁性
    2. 在优化之前,先进行性能分析,找到真正的瓶颈。
    3. 算法的选择比任何微优化都重要得多

普遍性优于特殊性

  • 原则简单化
  • 实践
    • 优先使用基本数据类型,而不是包装类。
    • 优先使用标准库,而不是自己实现。
    • 优先使用简单的、众所周知的算法和数据结构,而不是复杂的、炫技的。
    • 代码应该像故事一样,易于阅读和理解

第四部分:泛型

泛型是 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
  • 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 代替位域

  • 位域:使用 intlong 的位来表示一组标志。
  • 问题:类型不安全,需要手动处理位运算,API 丑陋。
  • EnumSet
    • 专门为枚举集合设计的高性能 Set 实现。
    • 内部使用位向量实现,性能媲美位域。
    • API 清晰,类型安全。
  • 最佳实践:如果需要表示一组枚举常量,EnumSet 是完美的选择。

EnumMap 代替序数索引

  • 问题:将枚举的 ordinal() 作为 List 或数组的索引。
  • 缺点:脆弱、浪费空间、效率低下。
  • EnumMap
    • 专门为枚举键设计的高性能 Map 实现。
    • 内部使用数组实现,访问速度极快。
    • API 清晰,类型安全。

慎用注解

  • 注解:为元数据提供了一种形式化的方法,不影响代码的语义。
  • 适用场景
    • 框架:如 Spring (@Autowired), JUnit (@Test)。
    • 代码生成工具:如 Lombok, ButterKnife。
    • 替代模式:如 @Override
  • 实践
    • 编写自己的注解类型:需要定义 @interface,并使用 @Target@Retention 元注解来限定其使用范围和生命周期。
    • 使用 @Documented 使注解出现在 Javadoc 中。
    • 使用 @Repeatable 允许在同一声明上多次使用同一个注解(Java 8+)。

第六部分:异常

只针对异常的情况才使用异常

  • 错误用法:将异常用于正常的控制流。
    // 错误:用异常来处理 for 循环的终止条件
    try {
        int i = 0;
        while (true) {
            range[i++].doSomething();
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        // ...
    }
  • 正确用法:异常只用于处理那些真正异常不期望发生的事件。

对可恢复的条件使用受检异常,对编程错误使用运行时异常

  • 受检异常:在方法签名中声明(throws),调用者必须处理(try-catchthrows)。
    • 适用场景:可预见的、可恢复的错误,如 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);
    }

方法抛出的所有异常都要有文档

  • 目的:让调用者知道可能需要处理哪些异常。
  • 实践:使用 @throws Javadoc 标签。
    /**
     * @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 的隐式锁已经足够时。

并发工具优先于 waitnotify

  • wait()notify()
    • 容易用错,必须与 synchronized 配合使用。
    • 容易导致死锁和性能问题。
    • API 难以理解。
  • java.util.concurrent 中的高级工具
    • CountDownLatch:允许一个或多个线程等待一组事件发生。
    • Semaphore:控制同时访问某个资源的线程数量。
    • CyclicBarrier:让一组线程在到达某个屏障时阻塞,直到所有线程都到达。
    • Exchanger:允许两个线程在某个点交换对象。
    • BlockingQueue:提供了阻塞的 puttake 操作,是生产者-消费者模式的理想选择。

线程安全性的文档化

  • 目的:让其他开发者知道如何正确地使用你的类。
  • 实践
    • 明确地说明类及其方法的线程安全级别
    • 线程安全级别
      • 不可变:绝对安全。
      • 无状态:没有可变域,绝对安全。
      • 线程安全:所有方法都有内部同步,可以并发调用。
      • 条件线程安全:除了单个方法外,某些方法的组合需要外部同步。
      • 线程兼容:不是线程安全的,但可以安全地由外部同步。
      • 线程不安全:绝对不安全。
    • 在文档中说明哪些锁需要外部同步

慎用延迟初始化

  • 问题:延迟初始化(在第一次使用时才初始化)在单线程中很简单,但在多线程中很复杂。
  • 单线程解决方案
    private FieldType field;
    public FieldType getField() {
        if (field == null) {
            field = new FieldType();
        }
        return field;
    }
  • 多线程解决方案
    • 双重检查锁volatile + synchronized,Java 5+ 后 volatile 保证了可见性和禁止指令重排,是可行的。
    • 静态内部类模式(推荐):利用类加载机制保证线程安全和延迟初始化。
      
      
分享:
扫描分享到社交APP
上一篇
下一篇