Java-Concurrency-in-Practice 03
2025-8-3
| 2025-8-3
Words 11573Read Time 29 min
type
status
date
slug
summary
tags
category
icon
password

4 章核心脉络速览

主题
关键问题
实战关注点
对象组合
如何把多个已经线程安全的组件“拼”成更大、更复杂的单元?
尽量重用已经验证过的线程安全类;组合时要保持封装,避免把内部细节暴露给并发环境
线程安全类设计三步曲
1. 明确状态变量2. 写出对这些变量成立的不变式3. 为状态并发访问拟定同步策略
把这三步写进类级注释或外部设计文档,方便后续维护者快速判断“还能不能安全修改”

4 .1 设计线程安全的类

目标:让维护者 只分析这一类本身 就能确信它是线程安全的,而不必把全部程序装进脑子里。

1. 找出状态变量(state variables)

  • 从字段(field)下手
    • 原始类型字段 → 状态就是它们的 n 元组
      • 例:Point(x, y) 的状态是 (x, y)
    • 引用字段 → 状态“递归地”包含被引用对象的字段
      • 例:LinkedList 的状态 = 链表节点集合的状态之和
  • 不要遗漏静态可变字段(它们属于“类状态”)
🎯 实践要领:在代码注释里给出“本类状态一览表”,以后别人加字段时就知道必须同步地更新文档。

2. 明确不变式(invariant)

  • 定义:任意时刻都必须成立的布尔表达式
  • 常见不变式示例
    • 计数器 value ∈ [0, Long.MAX_VALUE]
    • 点对象 distance = sqrt(x² + y²)(如果这是类的公开不变式)
  • 用途
      1. 指导“哪些修改必须是原子的”
      1. 让单元测试能写出“事后断言”验证并发正确性

3. 制定并发访问策略(synchronization policy)

  • 主体内容
    • 哪些变量由哪把锁 保护
    • 哪些方法需要同步、哪些可并行
    • 是否借助线程封闭不可变对象简化同步
  • 写法建议
    • Java 注解 + 注释并用
      • 在类级 Javadoc 中再补一句:“valuethis 内部监视器保护;所有复合操作必须先获取该锁。”

    代码范例拆解——Counter

    设计元素
    解释
    状态变量
    value
    不变式
    0 ≤ value ≤ Long.MAX_VALUE
    同步策略
    采用“Java 监视器模式”:进入任何公有方法前先获得实例锁 this;保证复合操作 (check ⇒ increment) 的原子性
    ⚠️ 如果以后有人打算加一个 reset() 方法,就必须继续遵守“先拿 this 锁再改 value”这一约定,否则会破坏不变式。

    小结与实践建议

    1. 封装就是并发范围控制器
        • 把可变状态藏在私有字段中,对外只暴露经过同步的访问入口。
    1. 文档是同步策略的“法律条文”
        • 没有文档,维护者只能猜测:这是线程封闭?可重入锁?条件变量?——猜错就埋雷。
    1. 组合优先于继承(在并发语境下尤甚)
        • 继承往往暴露内部实现,子类容易破坏父类不变式;组合可在外层重新制定独立的同步策略。
    接下来如果你继续贴出第四章的后续内容(如“委托、封闭容器、自定义同步器”等),我会按同样节奏帮你把关键动机、使用时机与典型代码模式梳理出来。
     

    4 .1 .1 收集同步需求 —— 把“应该同时发生”和“绝不能发生”都写清楚

    这一小节的精髓:
    先把业务语义里对“状态合法性”与“状态跃迁”的要求全部列出来,再决定要不要、以及如何加锁或用其它并发手段。只有这样,后面的同步策略才不会遗漏。

    1. 先画出“状态空间”与“非法区”

    术语
    说明
    为啥重要
    状态空间(state space)
    对象所有字段在理论上可能取到的笛卡尔积
    确定“潜在”风险:空间越大,需要排除非法状态的可能性越多
    不变性条件(invariant)
    系统在任何可见时刻都必须满足的布尔表达式
    违反它=线程安全 Bug
    后验条件(post-condition)
    某个操作做完以后 额外 必须满足的条件
    指出“状态跃迁”合法性的约束
    ⚡ 关键点:
    多用 final 和不可变类型可以缩小状态空间 —— 极限就是“只存在一个合法状态”,那么并发就简单到不需要同步(完全不可变)。

    2. 举例拆解

    (1) Counter 再进阶

    • 状态空间初始Long.MIN_VALUE … Long.MAX_VALUE
    • 不变式value ≥ 0
    • 后验条件increment() 必须把状态从 n 变到 n + 1
      • increment() 必须是复合原子操作(读-改-写不可拆)

    (2) NumberRange——典型“一对相关变量”问题

    坏版本(演示用):
    • 不变式跨越两个字段:lower ≤ upper
    • 如果两个 setter 各自加锁但用 不同 的锁,就会出现:
        1. 线程 A setLower(5),线程 B setUpper(4) ↔ 中间时刻存在 (lower=5, upper=4) —— 数据瞬间非法
        1. 线程 C isInRange(4) 读到非法状态,返回错误
    同步需求提炼
    需求
    代码层面的对策
    任何时刻都满足 lower ≤ upper
    两个字段 必须由同一把锁保护,或改成不可变对一起替换
    setLower / setUpper 更新时要一次性完成
    (a)使用 synchronized 且锁对象相同(b)使用 AtomicReference<IntPair> 整体替换
    改进示范(简化写法):

    3. 步骤化“收集同步需求”

    1. 列字段 → 标注可变 / 不可变 / final
    1. 写不变式 → 看看跨了几个字段
    1. 写每个公有方法的后验条件 → 判断是否依赖“旧值”
    1. 对照 1-3 → 推导“哪些操作必须原子”,“哪些字段要捆绑”
    1. 折中考量
        • 若无不变式约束 & 无旧值依赖 → 可以放宽同步,提高吞吐量
        • 否则坚持封装 + 原子性,哪怕牺牲一点性能

    4. 高阶提示

    • “先封装,后并发”:先把可能非法的状态对外屏蔽,再讨论锁;否则易漏。
    • 读多写少?考虑更细粒度锁或 StampedLock / rw-lock
    • 组合而非继承:子类往往不可见父类的不变式,最容易破坏原有同步假设。
    • 文档先行:把上述 1-3 步写在 Javadoc @implNote 里,维护者读一次就能知道安全边界。

    4 .1 .2 依赖状态的操作(State-dependent Operations)

    一句话抓重点
    当一个方法只有在“对象当前状态符合某个前提”时才能执行,它就是“依赖状态”的——在并发环境下不能直接失败,而应该等到前提变真,然后再继续。

    1. 先验条件 vs. 不变式 / 后验条件

    概念
    触发时机
    例子
    同步难点
    不变式
    任何时刻都必须成立
    lower ≤ upper(NumberRange)
    违反即 bug
    后验条件
    操作完成后必须成立
    increment()n → n + 1
    需原子地实施
    先验条件(pre-condition)
    操作开始前需要满足
    队列非空才能 dequeue()库存足够才能 reserve(n)
    可能暂时不成立;要等待

    2. 为什么“等一等”而不是“立刻失败”?

    • 单线程:先验条件假了 → 没人会改状态 → 直接抛异常
    • 并发:其他线程可能很快让条件变真
      • 如果直接失败 = 忙失败 → 不必要的重试/浪费
      • 正确做法:阻塞或挂起,直到条件满足再继续

    3. 实现依赖状态的三条路线

    路线
    何时用
    关键 API
    优缺点
    现成阻塞数据结构
    队列、栈、映射…
    java.util.concurrent 中的 BlockingQueue, LinkedBlockingDeque, SynchronousQueue
    最简单;自带容量/空满条件的等待逻辑
    同步工具类
    计数/许可/栅栏…
    Semaphore, CountDownLatch, CyclicBarrier, Phaser
    适合需要“资源槽”或“阶段”语义的先验条件
    显式条件队列
    定制化复杂条件
    Lock + Condition(或 wait/notifyAll
    灵活但易错;14 章专门讲
    💡 建议:除非真有非常规需求,优先用库,不要手写 wait/notify。

    4. 典型代码速览

    4.1 队列非空才能取 —— 用 BlockingQueue

    • take() 内部已封装先验条件:队列非空
    • 不需要显式加锁或 wait/notify,也避免忙轮询。

    4.2 资源不足就等 —— 用 Semaphore

    • 先验条件:当前可用连接数 > 0。
    • Semaphoreacquire()/release() 已帮你实现等待与通知。

    4.3 复杂条件示例(简版示意)

    如果条件是“缓存里至少有 K 条目系统在活跃模式”,可能同时依赖多个字段,这时:
    • 需要在 while 循环里重新检查条件(防止虚假唤醒)。
    • 这个模式易踩坑(忘记循环、搞错锁、只 signalsignalAll 等),因此更推荐把需求改写成能用现成工具类描述的形式。

    5. 小结 - 设计 checklist

    1. 列出现有/新增方法的先验条件
    1. 问:如果条件暂时为假,该方法该怎么做?
        • 放弃→简单返回/异常
        • 等待→进入“依赖状态”范畴
    1. 优先选用
        • BlockingQueue / TransferQueue / DelayQueue
        • Semaphore, CountDownLatch, CyclicBarrier, Phaser
    1. 只有在这些工具都无法表达你的条件时,再考虑 Conditionwait/notify,并遵循:
        • 始终在 while 循环里等待
        • 保护条件变量的那把锁必须一致
        • 优先 signalAll(),避免遗漏等待者
    🔮 预告:
    第 5 章 会系统介绍这些阻塞容器和同步原语的用法;
    第 14 章 再深入到 Conditionwait/notify 及显式锁实现细节。继续阅读时,可把本节当作“需求收集指南”。

    4 .1 .3 状态的所有权(Ownership)——“谁”真正控制这份数据?

    核心问题
    在一张“从 this 可达的对象图”里,哪些字段算“我的状态”? —— 只有我拥有(own)的那部分。所有权决定了封装边界加锁策略:谁拥有,就由谁来保障线程安全。

    1 | 为什么只说“子集”?

    • 对象图:从根对象出发,可达的所有节点
    • 对象状态:只取图中的自有数据
      • HashMap 拥有其 Entry 节点 ⇒ 算状态
      • 如果把外部传进来的 DataSource 放在字段里,但不拥有它(只是引用) ⇒ 属于本对象状态
    判定标准
    问题
    “是” ⇒ 属于状态
    “否” ⇒ 只是一条指针
    对象逻辑上由当前类创建/维护吗?
    该数据的并发一致性由当前类负责吗?
    释放内部引用后,调用方就无法再直接操作它吗?

    2 | 所有权 × 封装 × 同步 —— 三位一体

    设计元素
    关系
    封装
    把“拥有的数据”隐藏在私有字段里,对外仅通过受控方法暴露
    所有权
    决定能否 独占 修改权;若泄露引用只能变成“共享所有权”
    同步策略
    谁拥有谁上锁(或保证不可变 / 线程封闭)

    典型坑:泄露可变引用

    • 一旦别人拿到 list 的引用,原类就失去独占所有权 → 原有锁策略失效 → 数据竞争
    防御手段
    1. 返回不可变视图
      1. 只暴露只读接口Iterable 而非 List
      1. 深度拷贝 / 防御性拷贝:在构造函数接收外部集合时先 new ArrayList<>(arg),把所有权转移进来。

      3 | 传入对象:默认“不属于我”

      来源
      默认所有权
      线程安全对策
      构造器参数
      调用方 → 我方仅持引用
      若要长期保存:① 复制得到私有实例;② 明文说明“调用方不得再改”;③ 或假定对象本身线程安全
      普通方法参数
      同上(短期使用)
      通常只在方法栈里使用完即弃;无需同步
      专门的“工厂/封装”方法(如 Collections.synchronizedMap)
      通过文档契约把所有权转移给被创建对象
      调用者不得再直接访问原实例

      4 | 共享所有权:只能用协商同步

      • 当多个组件都需要直接修改同一份数据时 → 本类无法单独保证不变量
      • 解决方案
          1. 让数据对象自身线程安全(如 ConcurrentHashMap
          1. 共同使用同一把显式锁(锁对象向所有参与者公开)
          1. 消息传递 / 不变数据:通过复制和交换事件,避免共享可变数据

      5 | Servlet Session 示例说明

      Web 容器可能在“会话复制”或“钝化/passivation”阶段序列化 HttpSession 里的对象;此时容器线程与应用线程并发访问同一数据。
      • 含义
        • HttpSession 拥有其属性 Map;却把属性对象的引用共享给容器线程
        • ⇒ 属性对象本身必须是线程安全的/不可变的,或者应用明确提供同步委托
      • 实战建议
        • 存入 Session 的对象最好设计为 可序列化且不可变
        • 若属性需要可变,使用并发容器或自行管理锁
        • 避免把 JPA/Lazy-loaded 实体等非线程安全对象直接塞进 Session

      6 | 设计-检查清单

      1. 列出字段 → 标记“拥有/非拥有”
      1. 对“拥有 + 可变”的数据:
          • 私有化 → 拒绝外部引用泄露
          • 制定并记录锁规则 (@GuardedBy) 或使用不可变封装
      1. 对“非拥有”的可变数据:
          • 仅短期使用;或复制;或要求其自身线程安全
      1. 公有 API 里不要返回内部可变对象;若必须返回,给“只读视图”或深拷贝
      1. 若必须共享所有权:文档里声明同步协议,或用并发容器

      下一节若继续探讨“委托(Delegation)、线程封闭(Thread Confinement)、组合模式”等,请继续贴原文;我会沿用概念提炼 → 代码示例 → 实战指引的结构帮你梳理。

      Ownership(状态所有权)是什么?

      一句话:谁对某块可变数据拥有 独占 决策权,就谁负责在并发环境下保护它的完整性与可见性。

      1 | 为什么要强调所有权

      场景
      没有清晰所有权时会怎样?
      线程安全
      多个对象“以为”自己掌控同一份数据 → 锁策略各自为政 → 数据竞争或死锁
      封装性
      外部直接改内部字段,类无法维护不变式
      可维护性
      维护者看不出“到底谁应该同步”,轻易打破既有假设

      2 | 三类常见所有权模型

      模型
      Java落地方式
      线程安全保证思路
      独占所有权(Exclusive)
      私有字段 + 不泄露可变引用
      类内部自己加锁/确保不可变
      共享所有权(Shared)
      把对象引用暴露给多个组件
      必须协商:同用一把锁,或对象本身线程安全 (ConcurrentHashMap)
      转移所有权(Transfer)
      通过方法/队列把引用交出去,自己不再使用
      通常用阻塞队列、Future等“一手交钱一手交货”的机制;完成即解除旧锁责任
      📌 思维辅助——可对比 C++/Rust:
      • unique_ptr ≈ Java 的独占所有权;
      • shared_ptr/Arc ≈ 共享所有权;
      • Rust 的 move 语义、消息传递(channel)≈ 转移所有权。

      3 | 在 Java 中识别 & 记录所有权

      1. 字段注解
        1. Javadoc @implNote / @param ownsXxx true if ownership is transferred
        1. 工厂方法命名ofOwned, wrapShared 等提示调用方约定。
        1. 包 / 访问级别:把“仅供内部线程使用”的类放到私有包或用包可见(不 public)。

        4 | 设计技巧与示例

        4.1 独占所有权:封装 + 私有锁

        4.2 避免引用泄露导致失去所有权

        修复方式
        • 返回 不可变视图Collections.unmodifiableList(list)
        • 深拷贝new ArrayList<>(list)
        • 只暴露 只读接口Iterable<String> items()

        4.3 共享所有权:让被共享对象自带并发行为

        若使用普通 HashMap,就必须约定“外界也用同一把 lock”。

        4.4 转移所有权:阻塞队列/消息通道

        • 阻塞队列天然实现“先验条件:队列非空”与“显式交接权”。

        5 | 与其他并发策略的关系

        策略
        与所有权的耦合点
        线程封闭 (Thread Confinement)
        把数据“独占”给单线程 → 问题简化;所有权明确为该线程
        不可变对象
        把状态“冻结” → 没有写权,就无需谈锁;所有权退化为“只读共享”
        Actor / Event-Loop
        “每个 Actor 只改自己的状态” → 把所有权绑定到消息循环线程

        6 | 排雷清单

        构造器参数是外部传入的可变对象?→ 拷贝再保存
        返回值是否直接泄露内部可变结构?
        把同一对象字段暴露给多线程时,是否明确锁协议或采用并发容器?
        文档里是否写明“谁负责同步”?没有就默认调用方 不能 并发更改。
        需要序列化/钝化(如 HttpSession)?→ 属性对象本身得线程安全或不可变。

        7 | 小结

        • Ownership = 控制权 = 锁权责。确认谁拥有数据,就能确定谁加锁、谁维护不变式。
        • 独占 → 私有 & 封装共享 → 协商同步或并发容器转移 → 明确交接机制
        • 把所有权信息写进代码注释与 API 设计中,才能防止后续修改者无意间破坏线程安全。

        4.2 实例封闭

        一、概念与动机

        1. 什么是实例封闭?
            • 也称“实例级封闭”(Instance Confinement),指将一个本身非线程安全的对象,限制在单个线程或受控的访问路径中使用,从而无需修改该对象也能实现线程安全。
            • 本质是在使用端,对对象的所有访问都加以约束——要么只在一个线程里访问(线程封闭),要么通过同一个锁来保护(锁封闭)。
        1. 为什么要实例封闭?
            • 简化线程安全实现:不用对原始类做任何假设或改造,直接在“外部”通过封闭策略保证安全。
            • 灵活的加锁策略:可以根据不同状态变量或访问场景,选用不同的锁来保护不同对象。

        二、实现手段与典型示例

        1. 单对象加锁(锁封闭)

        示例代码(PersonSet)
        • mySet是一个非线程安全HashSet,但由于它被私有化且所有对它的访问都必须持有PersonSet.this锁,因而PersonSet对外表现为线程安全类。
        • Person本身的线程安全不做假设——如果Person可变,取出后访问仍需额外同步。

        2. 线程封闭

        • 将对象仅在单个线程内部创建和使用(典型如方法内的局部变量或只在线程内部流转),自然线程安全,无需任何锁。

        3. 装饰者模式(Decorator)

        • Java 标准库对常用容器(如ArrayListHashMap)提供了“同步包装”:
          • 通过“装饰者”将非线程安全容器封闭到一个持有唯一引用的同步代理里,代理中每个方法都加锁,从而对外表现线程安全。

          三、封闭的作用域与注意事项

          封闭方式
          作用域示例
          优点
          风险
          私有成员封闭
          对象作为类的私有字段
          易实现、便于分析
          如果对象引用泄漏,封闭失效
          局部变量封闭
          方法内部创建并仅在线程内传递
          最简洁、零锁开销
          作用域小,不适用于长生命周期
          线程级封闭
          仅在同一线程中创建/传递
          无需加锁
          必须严格保证不跨线程使用
          装饰器封闭
          Collections.synchronized* 系列包装器
          直接可用、开箱即用
          同步代价、可能死锁
          • 作用域不能超出既定范围:只要封闭对象的引用被意外发布(例如存入全局集合、内部类泄漏等),原有的线程安全假设就会失效。

          四、与其他锁策略的结合

          • 多锁保护:在大型系统中,不同状态变量可以用不同锁来保护。实例封闭为每个受保护对象提供了灵活的“最小粒度锁”。
          • 与 ServerStatus 等示例对比:后续章节的 ServerStatus 会演示如何给一个类的多个可变状态分别使用不同锁(而不是简单地给整个对象加一把大锁)。

          五、小结

          • 实例封闭是构建线程安全类的最简单方式之一,对象内部状态通过私有化 + 统一访问路径得到有效保护。
          • 既可以结合加锁(锁封闭),也可以依赖线程封闭机制,或使用装饰者模式对第三方库进行封闭。
          • 关键在于:绝不让非线程安全对象的引用在未经保护的情况下流出

          4.2.1 Java 监视器模式(Monitor Pattern)


          1. 模式概述

          • 核心思想:将对象的所有可变状态封装在对象自身,并由该对象的内置锁(monitor)来保护。
          • 实现机制:在 Java 中,每个对象自带一把监视器锁,进入 synchronized 方法/块时由 JVM 字节码指令 monitorenter 加锁,退出时由 monitorexit 解锁。

          2. 典型用法

          2.1 内置锁(this

          • 优点:最简洁 —— 只需在方法前加 synchronized
          • 缺点:锁对象(this)是公有的,可能被外部误用。

          2.2 私有锁对象

          • 优点:锁完全私有,外部无法干预,便于分析和维护。
          • 缺点:代码稍微冗长,需要显式声明和使用锁对象。

          3. 性能 & 可维护性对比

          特性
          内置锁(this
          私有锁对象
          代码复杂度
          ★☆☆(最简单)
          ★★☆(需声明锁字段)
          锁的可见性
          公有,可能被外部代码意外获取
          私有,仅类内部可见
          安全性分析范围
          必须全局审查是否被误用
          只需审查本类内部
          活性/死锁风险
          外部也能加锁,易引入活性问题
          客户端无法干预,风险更低
          伸缩性/细粒度
          粗粒度锁,可能成为性能瓶颈
          可按状态拆分多把私有锁

          4. 何时选用

          1. 快速加锁:类内部状态较少,对一把大锁的性能开销可接受。
          1. 最低样板:希望以最少的代码实现线程安全(如 JDK 的 VectorHashtable)。
          1. 后续优化:将来如需更精细的并发控制,可再引入读写锁、分段锁等。

          5. 下一步拓展

          • 可视化时序图:展示 monitorenter/monitorexit 在方法执行中的加解锁流程。
          • 性能对比实验:监视器模式 vs. 实例封闭(锁封闭)在高并发场景下的吞吐对比。
          • ServerStatus 示例:演示如何用多把私有锁分别保护不同状态字段,以提升并发度。

          4.2.2 示例:基于监视器模式的“车辆追踪器”(Vehicle Tracker)


          1. 背景与需求

          • 应用场景:MVC GUI 程序中,一个视图线程(只读)和多个更新线程(写入)并发访问/修改“出租车、警车、货车”等车辆的位置数据。
          • 设计目标
              1. 对外提供获取所有车辆位置的快照。
              1. 提供按 ID 获取单个车辆位置的方法。
              1. 提供更新某辆车位置的接口。
              1. 保证在多线程环境下线程安全。

          2. 核心代码(程序清单 4-4)


          3. 线程安全策略

          1. 监视器锁
              • MonitorVehicleTracker 的所有关键方法都被 synchronized 修饰,使用自身监视器锁保护内部状态 locations
              • 外部无法直接访问或修改 locations 或其元素。
          1. 深拷贝快照
              • 构造时、getLocations() 时均做全量深拷贝,并返回 不可修改的映射视图
              • getLocation(id) 也返回一个新的 MutablePoint 拷贝,避免暴露内部可变对象。

          4. 深拷贝策略的权衡

          特性
          优点
          缺点
          一致性
          外部拿到的快照在整个调用期间内保持不变,内部状态不会被并发写修改影响
          更新后要查看最新位置,必须重新调用快照方法
          安全性
          避免了对内部 MutablePoint 对象的直接修改
          无需额外锁;MutablePoint 本身可变但未被发布
          性能
          对少量车辆或低频调用足够;深拷贝逻辑集中、易于理解
          车辆数量大或调用频繁时,复制开销显著;复制期间持有锁会阻塞写操作
          响应延迟
          视图线程拿到快照迅速渲染
          deepCopy 运行期间,更新线程必须等待

          5. 缺点与优化方向

          1. 性能瓶颈
              • deepCopy 在大数据量时耗时,且在 synchronized 块内执行,影响写操作的并发性。
              • 改进:可改用 读写锁ReentrantReadWriteLock)——读时并发,写时独占;或用 基于并发容器ConcurrentHashMap<String, Point>)配合不可变坐标对象ImmutablePoint)来避免全量复制。
          1. 数据新鲜度
              • 快照模型保证了一致性,但调用者若需“实时”位置,则需要更高的刷新频率或直接读取内部数据。
              • 改进
                • 提供单车 可变视图(但需额外同步);
                • 或者用发布/订阅机制,更新时主动推送给视图线程。
          1. 封装性放宽
            1. 下一节(4.2.3)将演示如何放松封装(比如直接公开线程安全的 Map),同时仍保持线程安全——例如返回一个实时视图而非快照,或在不复制 Map 键集合的前提下依然保证安全。

          小结:
          • 本示例展示了如何用最简单的监视器模式+深拷贝策略,构造一个线程安全且易于理解的数据模型;
          • 同时也凸显了“安全 vs. 性能 & 时效性”的典型权衡。
          • 后续章节将探讨更高性能/更灵活的设计。

          4.3 线程安全性的委托(Delegating Thread Safety)


          1. 背景:组合已有线程安全组件

          在设计一个新类时,如果它内部所用的所有组件已经是线程安全的,是否还需要给新类再加一层锁?答案——“视情况而定”。

          2. 委托模式的要点

          • 状态唯一且无额外约束:如果新类的可变状态完全对应于某个已线程安全的组件,且不需要在组件之上再施加复合不变式(比如同时更新多个组件保持一致性),那么新类可以把“线程安全”委托给那个组件。
          • 无需额外加锁:不必用 synchronized 或私有锁包装,只要不打破组件本身的语义,就能保持线程安全。

          3. 典型示例:CountingFactorizer

          • 分析
            • count 是唯一的可变状态,由 AtomicLong 管理。
            • CountingFactorizer 不在 count 之外维护任何额外不变式,也不做复合操作(例如“先读再写”)。
            • 因此,它的线程安全性完全依赖于 AtomicLong

          4. 何时“组合即线程安全”?

          情况
          可否直接委托
          组件本身线程安全,且类对组件状态无额外不变式,不做跨组件复合操作
          ✅ 直接组合,无需额外锁
          组件间存在不变式(如同时更新两个组件的状态必须保持一致)
          ❌ 需要在类层面增加锁或其他同步手段,保证原子性

          5. 小结与后续

          • 委托线程安全 是最轻量的并发策略:把并发控制责任交给成熟、经过验证的组件。
          • 局限:只适用于“状态单一且无跨组件约束”的场景;否则需要额外同步。
          • 后续章节(如程序清单4-10)会给出“组件都是线程安全的,但组合后仍需自己加锁”的典型反例,帮助你理解何时必须介入更高层次的同步控制。

          4.3.1 示例:基于委托的车辆追踪器


          1. 不可变坐标类(Point)

          • 线程安全性:由于所有字段都是 final 并在构造时初始化,Point 本身即为线程安全,可自由共享与发布,无需复制。

          2. 委托给 ConcurrentHashMap 的追踪器

          • 无任何显式同步:所有并发安全性由 ConcurrentHashMap 与不可变 Point 保证。
          • 封装性unmodifiableMap 防止外部直接修改 Map 结构;Point 不可变,防止修改坐标。

          3. 实时视图 vs. 快照视图

          特性
          基于监视器模式(MonitorVehicleTracker)
          基于委托模式(DelegatingVehicleTracker)
          返回的数据
          快照(snapshot)
          实时视图(实时反映后续更新)
          视图的一致性
          一致性强,不随后续写操作改变
          随后续写操作更新,有“可见性延迟”或“实时一致性”
          性能开销
          深拷贝 + 同步锁,调用频繁或数据量大时成本高
          只做替换操作,无全量复制,性能更优
          适用场景
          需要对一组数据保持内部一致性
          需要及时反映最新位置,且接受可能的“视图不一致”

          4. 提供快照的浅拷贝方案

          如果既想要“实时性能”又想要“一致快照”,可以在返回时做一次浅拷贝(复制 Map 结构,不复制 Point):
          • 由于 Point 是不可变类型,复制 Map 结构即可保证读者拿到的“快照”不随后续写操作而改变。

          5. 小结

          • 委托策略:将并发控制完全交给成熟的线程安全组件(ConcurrentHashMap + 不可变对象),无需在外层再加锁。
          • 行为差异:与监视器模式(快照)相比,委托模式可实现更高的并发吞吐和更低的延迟,但返回的是“实时变化”视图。
          • 选择依据:如果需要最新数据且可容忍视图抖动,优先使用委托模式;否则在委托模式上再做一次浅拷贝,平衡一致性与性能。

          4.3.2 将线程安全性委托给独立的状态变量


          一、核心思想

          • 只要组件之间没有不变性约束(invariant)需要同时保持,就可以各自为政,将线程安全委托给它们各自的线程安全实现。
          • 这种做法比给整个类加一把大锁更加灵活,也无需任何额外的同步开销。

          二、典型示例:VisualComponent

          • 数据结构选择CopyOnWriteArrayList
            • 读操作无需加锁,遍历时可放心读旧快照;
            • 写操作(增删监听器)会复制底层数组,适合“写少、读多”的监听器场景。
          • 委托缘由
              1. keyListenersmouseListeners 之间互不影响,不存在“同时修改两个列表必须保持一致”的额外不变式;
              1. 每个列表自身已保证线程安全,VisualComponent 无需再加锁。

          三、优势与适用场景

          特性
          说明
          零锁开销
          没有 synchronized 或显示锁对象,读写都依赖组件自身机制。
          高并发性能
          读操作(事件分发)无需阻塞,写操作(增删监听器)在低频场景下成本可接受。
          设计简洁
          不用关心全局锁的持有与释放,只需组合线程安全组件。
          易于扩展
          新增另一种监听器类型,只需再声明一个线程安全的集合,无需修改现有同步策略。
          适用场景
          • 组件内部的多个可变子状态之间无复合不变式
          • 子状态都可以独立、安全地并发访问与修改;
          • 读多写少、或对写性能要求不高。

          四、何时不适用

          • 如果两个或多个状态变量必须一起修改才能保持一致性(例如:同时在 keyListenersmouseListeners 中添加/删除某个“复合监听器”),就需要引入额外的同步机制(如 synchronizedLock 或事务式更新)来保证跨组件的原子性。
           

          4.3.3 当“委托”失效时


          1. 背景:跨变量不变式(Invariant)

          • NumberRange 类通过两个独立的 AtomicInteger 管理“下界”(lower)和“上界”(upper)。
          • 不变式要求:始终满足 lower ≤ upper
          • 虽然单个 AtomicInteger 是线程安全的,但二者组合后产生了“跨变量不变式”,单纯委托给这两个原子变量并不能保证这个不变式。

          2. 问题代码(错误示例)

          • 竞态场景
            • 线程 A 调用 setLower(5) 时,读到 upper = 10,检查通过;
            • 与此同时,线程 B 调用 setUpper(4) 时,读到 lower = 0,检查也通过;
            • 最终,lower = 5upper = 4,违反了不变式。

          3. 失效原因分析

          原因类别
          说明
          非原子性检查-执行
          “先检查后执行”跨越了两个独立的原子操作,缺乏整体原子性。
          跨变量依赖
          lowerupper 之间存在依赖关系,单独保证每个变量的线程安全并不能保证二者组合的正确性。

          4. 解决策略

          4.1 使用同一把锁保护复合操作

          • 原理
            • 将读-改-写的整个检查-执行序列包裹在一个 synchronized 块中,保证操作的原子性可见性
            • 只要所有访问都使用同一把锁,lower ≤ upper 不变式就始终成立。

          4.2 使用显式 ReentrantLock

          • 优点:可中断锁获取、可尝试锁、可公平性配置。

          5. 何时需要介入“额外同步”

          场景
          做法
          跨组件/跨变量不变式
          在外层类增加锁或其他同步机制
          复合检查-执行
          把检查与执行包裹在同一同步上下文中
          需要一致快照或原子更新多个状态
          使用大锁、读写锁、事务式更新机制

          6. 小结

          • AtomicXxx 或其他线程安全组件并不能解决所有并发场景——只有在“状态相互独立”时,才可放心委托。
          • 跨变量依赖 会破坏单一组件的线程安全性,必须由外层类提供自己的并发控制。
          • 下次遇到“先检查后执行”模式,务必确认是否需要整体原子性——若需要,立刻引入合适的锁机制。

          提示:如果你想了解更高级的模式(如使用事务内存或乐观 & 悲观锁组合)来维护复杂不变式,或演示“读-写锁下的并发性能对比”,请告诉我!

          4.3.4 发布底层的状态变量(Publishing Underlying State Variables)


          当把线程安全性委托给底层状态变量时,只有在不会破坏类所维护的不变性(invariant)时,才可以公开或发布这些变量,让外部直接访问或修改。

          1. 发布的必要条件

          1. 线程安全性由该变量自身保证
              • 变量类型本身必须是线程安全的(例如 ConcurrentHashMapCopyOnWriteArrayList、不可变对象、原子类型等)。
          1. 无跨变量不变性约束
              • 类中不再对该变量与其他状态变量之间施加任何不变性条件。
              • 外部对它的任何修改都不会影响其他状态,也不会走入非法状态。
          1. 不存在“不允许的状态转换”
              • 该变量的任何合法操作(增、删、改)都被类逻辑所接受,无需额外检查或补偿。

          2. 典型对比示例

          场景
          是否可发布
          原因
          Counter.value (AtomicLong)
          类约定 value ≥ 0,且只有“递增”操作;外部若直接写入负值,会破坏不变性。
          当前温度(TemperatureSensor)
          ✔️
          温度可为任意值,且类对其值域没有额外约束;外部修改不会破坏不变性。
          VisualComponent.listeners
          ✔️
          CopyOnWriteArrayList 本身线程安全;不同监听器列表互不依赖,无额外不变式。

          3. 实践建议

          • 尽量保持封装
            • 即便“可以发布”,为了未来演进和子类化的灵活性,仍建议通过访问方法而非直接公开字段。
          • 文档清晰
            • 如果决定发布某个线程安全字段,务必在类注释中明确说明该字段的线程安全特性和不变性保证。
          • 审查不变性
            • 发布前,仔细检查类中所有逻辑,确保对该变量的所有使用场景都不依赖其与其他状态的一致性。

          小结:
          发布底层状态变量是一种权衡——在满足“单一且独立状态”条件下,可以安全地公开它们以简化设计;但在任何“跨状态不变性”存在时,都必须避免发布,或者引入额外的同步机制来维护不变性。

          4.3.5 示例:发布状态的车辆追踪器


          1. 目标与背景

          • 前面我们讨论了两种追踪器设计:
              1. MonitorVehicleTracker:用监视器锁+深拷贝,返回一致的快照;
              1. DelegatingVehicleTracker:委托给 ConcurrentHashMap+不可变 Point,返回实时视图。
          • 这里引入第三种:PublishingVehicleTracker,其核心思想是
            • 发布(expose)底层可变且线程安全的状态对象;
            • 保证线程安全的同时,允许外部直接修改坐标。

          2. 可变线程安全的 SafePoint

          • 线程安全性
            • x,ysynchronized 保护,get()set() 是原子操作;
            • 私有构造函数模式(Private Constructor Capture Idiom)保证从现有实例拷贝时不被并发修改。

          3. 发布状态的追踪器:PublishingVehicleTracker

          • 线程安全性完全委托给:
            • ConcurrentHashMap 保证对 locations 结构的并发访问;
            • SafePoint 保证对单个坐标的并发读写。
          • 发布状态getLocationgetLocations 均返回对底层可变对象的引用,外部可直接调用 set(...)
          • 实时性:所有坐标更新立即可见,无需额外刷新;与 DelegatingVehicleTracker 类似,返回的是“实时视图”。

          4. 适用性与风险

          特性
          PublishingVehicleTracker
          实时性能
          更新后即刻可见,无拷贝开销
          线程安全性
          依赖内部组件,不用外层加锁
          封装性降低
          发布了底层可变状态,外部可随意修改 SafePoint 内容
          可施加约束
          如果需要“范围校验”或“触发动作”,此设计不适用
          演进限制
          将来若需在位置变更时做额外操作(如事件通知),需重构

          5. Private Constructor Capture 模式

          • SafePoint(SafePoint p) 中调用私有构造函数 new SafePoint(p.get()),而非直接用 p.x,p.y
          • 避免在读取 p.xp.y 之间出现竞态,确保拷贝的是同一时刻的状态。

          6. 小结

          • PublishingVehicleTracker 是“发布底层可变状态”策略的经典示例。
          • 它适用于对性能与实时性要求极高,且业务对“封装性”或“校验”无额外需求的场景。
          • 一旦需要在更新时加入检查、触发回调或保持复杂不变式,就必须在外层引入同步或事件机制。

          拓展:
          • 如果你需要对比这三种追踪器在高并发场景下的吞吐与延迟,或希望看到它们的时序图UML 类图,请告诉我!

          4.4 在现有的线程安全类中添加功能


          1. 背景与动机

          • Java 标准库中已有大量经过验证的线程安全基础类,重用它们可以节省开发和测试成本。
          • 但有时,这些类缺少某些原子操作——例如“若不存在则添加”(put-If-Absent)——而我们又不能修改它们的源代码。

          2. 原子“若不存在则添加”需求

          • 功能定义
            • 如果集合中尚未包含元素 x,则将其添加;否则不做任何操作;并且保证在高并发环境下只会添加一次。
          • 线程安全要求
            • “检查(contains)”和“添加(add)”必须作为一个原子操作,否则两个线程可能都看到“尚未包含”,都执行添加,导致重复元素。

          3. 继承 Vector 的示例:BetterVector

          • 优点
            • 直接复用 Vector 已有的同步策略和大部分功能。
            • 在单一源文件内维护同步逻辑,便于阅读和调试。
          • 缺点
            • 脆弱:若 Vector 底层同步锁策略改变(例如改用不同锁或细粒度锁),子类中使用的 synchronized 就可能不再保护父类的内部状态。
            • 依赖于 Vector 公开其同步策略——大多数类(非 Vector)并不鼓励或保证这样做。

          4. 扩展 vs. 委托的风险对比

          特性
          继承 (BetterVector)
          委托(Wrapper)
          同步策略可见性
          依赖父类实现,若变更易导致破裂
          由自己控制锁或委托对象,无隐式依赖
          源代码位置
          分散在父类和子类两个文件
          全部在外层封装类中,易于维护
          对库演进的敏感度
          高——父类同步策略改变时容易失效
          低——只要委托对象接口不变即可
          可重用性
          仅限 Vector 或明确公开同步策略的类
          可对任意 List<E>Set<E> 等组合封装

          5. 推荐做法

          5.1 使用组合+显式同步

          • 锁完全由外层类控制,不依赖被封装类的同步策略。
          • 可对任何 List<E>(如 Collections.synchronizedListCopyOnWriteArrayList)进行封装。

          5.2 利用现有并发集合

          • 对于键—值存储,ConcurrentMap 已内置 putIfAbsent
            • 对于需要集合语义(无重复元素),可将 ConcurrentMap<E, Boolean> 作为底层实现。

            6. 小结

            • 扩展线程安全类(继承)来添加原子操作虽简单,但高度依赖父类同步实现,易受库演进影响。
            • 推荐组合+委托直接使用并发容器,由自己控制同步或利用成熟的并发 API,既安全又灵活。
            • 在设计新增操作时,始终确认“检查”与“执行”之间的原子性需求,选择最稳健的实现方式。

            4.4.1 客户端加锁机制(Client-Side Locking)


            1. 背景

            针对通过 Collections.synchronizedList(...) 获得的线程安全 List,我们既不能修改其源码,也无法通过继承来增加原子操作,因为客户端并不知道返回的 List 类型。
            因此,需要在客户端显式地使用与被封装对象相同的锁来保护复合操作。

            2. 错误示例:错误的同步对象

            • 问题所在
              • putIfAbsentsynchronized 修饰,使用的是 ListHelper.this 作为锁;
                list 本身的同步策略(内部使用的锁)是另一个对象(通常是 list 自身)。
            • 后果
              • 客户端操作和 list 内部操作并不在同一把锁保护下,仍然无法保证 containsadd 的原子性。

            3. 正确示例:基于客户端加锁

            • 关键点
              • synchronized(list)list 内部所有方法(如 add, contains)使用的是同一把锁,才能保证复合操作的原子性。
            • 效果
              • 在调用 putIfAbsent 时,containsadd 在同一锁范围内执行,外部任何线程在此期间都无法修改 list

            4. 客户端加锁的风险与局限

            优点
            缺点
            · 不需修改被封装类源码
            · 同步策略分散在客户端,易被误用
            · 可对任意 synchronized 封装器
            · 依赖外部代码“正确”遵循加锁约定
            · 难以控制多个客户端同时正确加锁
            • 脆弱性
              • 若封装器改变了内部锁对象(不再使用 this),客户端加锁就会失效;
              • 客户端代码必须“了解”并使用相同的锁,违反了封装性原则。

            5. 小结

            • 扩展类客户端加锁 都会破坏同步策略的封装性,相对更脆弱;
            • 推荐
              • 对于关键原子操作,更稳妥的做法是 组合+显式同步(在一个封装类里统一管理锁);
              • 或者直接使用更高级的并发容器(如 ConcurrentMap)中内置的原子方法。
             
             
          • 并发
          • 浮点数的编码Java-Concurrency-in-Practice 02
            Loading...
            Catalog
            0%