Skip to content

Java 集合契约、有界并发与JVM诊断 ​

前置是类型、集合与资源。使用Temurin JDK21.0.12.1+1,与现有课程兼容,不要求升级站点依赖;Windows PowerShell与Linux Bash均有独立验证。完整项目不依赖Maven、Gradle或服务端框架,依赖只有JDK标准库。

相等、哈希、泛型和集合视图 ​

HashMap以equals判定逻辑相等,hashCode用于定位桶;相等对象必须哈希相同,哈希相同却未必相等。键进入Map后参与相等和哈希的字段不能变化,否则原桶定位失效。本例record Key(String name)用不可变String组成值键;record若包含List等可变组件,只是引用final,不能因此声称深不可变。

List<? extends Number>适合读取为Number,通常不能加入Integer;List<? super Integer>适合写入Integer,读取只能视为Object。泛型擦除不是允许随意强转的理由,原始类型会把编译问题推迟到ClassCastException。项目使用明确的Future<Entry>,避免Object和unchecked转换。

List.copyOf在入口建立输入快照并拒绝null,防止调用者在任务执行中修改原列表。Map.copyOf生成不可修改结果;不能把它和Collections.unmodifiableMap对原Map的视图混淆。集合不可修改不等于元素不可变,本例键和Integer值也不可变。HashMap迭代顺序未约定,CLI显式按键排序以得到可重复输出。

队列容量、所有权与失败传播 ​

两条工作线程负责纯解析,主线程逐个Future.get并独占结果Map。这样不需要ConcurrentHashMap;加上并发容器也不能让多个相关字段自动成为一个事务。队列容量10000,入口最多10000条,不使用无界队列吞掉无限任务。每个任务输入是不可变String,没有共享可变状态。

提交前的操作对任务可见,任务结果通过Future.get发布给等待线程;这些happens-before关系解决可见性,volatile不让count++变成原子操作。锁适合复合不变式,原子变量适合明确的独立状态。sleep不是同步,不可拿延迟猜测另一个线程已经完成。

解析异常由get包成ExecutionException,累计整数溢出由Math.addExact直接抛ArithmeticException,不允许静默回绕。返回前只有局部Map,没有部分成功结果逃逸。首次观察到失败后取消未完成Future并关闭池;解析无阻塞且输入有上限,清理能结束。取消是中断请求,不会强杀忽略中断的任务;若换成网络调用,还必须配置请求超时及响应体关闭。

这是有界并发而非低延迟失败检测:按提交顺序get可能等待前面任务,后来任务即使先失败也要稍后观察。需要按完成顺序处理时可采用CompletionService,并单独定义结果顺序和失败协议;不能在本项目里默默改变接口语义。

完整Ledger项目 ​

将以下两个文件放examples/java/ledger/。输入格式是名称:非负整数,名称1至16个小写ASCII字母,数字1至10位且须落在int范围。重复名称累计,上限10000行。返回Map不可修改。所有源码只有一个public类对应文件名,无外部依赖。

java
import java.util.*;
import java.util.concurrent.*;
public final class Ledger {
    public record Key(String name) {
        public Key { if (name == null || !name.matches("[a-z]{1,16}")) throw new IllegalArgumentException("key"); }
    }
    private record Entry(Key key, int amount) {}
    private static Entry parse(String line) {
        var fields = line.split(":", -1);
        if (fields.length != 2 || !fields[1].matches("[0-9]{1,10}")) throw new IllegalArgumentException("entry");
        return new Entry(new Key(fields[0]), Integer.parseInt(fields[1]));
    }
    public static Map<Key,Integer> aggregate(List<String> lines)
            throws InterruptedException, ExecutionException {
        var input = List.copyOf(lines);
        if (input.size() > 10000) throw new IllegalArgumentException("too many entries");
        var pool = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(10000), new ThreadPoolExecutor.AbortPolicy());
        var futures = new ArrayList<Future<Entry>>();
        try (pool) {
            try {
                for (var line : input) futures.add(pool.submit(() -> parse(line)));
                var result = new HashMap<Key,Integer>();
                for (var future : futures) {
                    var entry = future.get();
                    result.merge(entry.key(), entry.amount(), Math::addExact);
                }
                return Map.copyOf(result);
            } finally {
                for (var future : futures) if (!future.isDone()) future.cancel(true);
                pool.shutdownNow();
            }
        }
    }
    public static void main(String[] args) throws Exception {
        var result = aggregate(Arrays.asList(args));
        result.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().name()))
                .forEach(e -> System.out.println(e.getKey().name()+"="+e.getValue()));
    }
}
java
import java.util.*;
import java.util.concurrent.*;
public class LedgerTest {
    static void check(boolean value) { if (!value) throw new AssertionError(); }
    public static void main(String[] args) throws Exception {
        var key = new Ledger.Key("a");
        check(key.equals(new Ledger.Key("a")) && key.hashCode()==new Ledger.Key("a").hashCode());
        var data = Ledger.aggregate(List.of("a:2", "a:3", "b:1"));
        check(data.equals(Map.of(new Ledger.Key("a"), 5, new Ledger.Key("b"), 1)));
        try { data.put(key, 8); throw new AssertionError(); } catch (UnsupportedOperationException expected) {}
        try { Ledger.aggregate(List.of("a:2", "bad")); throw new AssertionError(); } catch (ExecutionException expected) {}
        try { Ledger.aggregate(List.of("a:2147483647", "a:1")); throw new AssertionError(); } catch (ArithmeticException expected) {}
        check(Ledger.aggregate(List.of()).isEmpty());
        System.out.println("ledger: all contracts passed");
    }
}

仓库根目录创建build-ledger输出目录,执行以下命令。PowerShell工具路径含空格时使用& 'JDK/bin/javac.exe' ...;Bash按实际JDK路径执行,classpath这里只含单个目录,没有平台分隔符差异。

sh
javac -encoding UTF-8 -d build-ledger examples/java/ledger/Ledger.java examples/java/ledger/LedgerTest.java
java -cp build-ledger LedgerTest
java -cp build-ledger Ledger b:1 a:2 a:3
java -cp build-ledger Ledger bad

测试输出ledger: all contracts passed、退出0;有效CLI依次输出a=5、b=1。最后命令非零退出,stderr包含ExecutionException及非法输入原因,stdout为空;CLI保留异常链便于教学定位。测试不用assert关键字而显式抛AssertionError,忘记-ea也不会跳过检查。覆盖相等/哈希、累计、不可修改结果、空输入、解析失败和溢出。

资源作用域与中断 ​

JDK21的ExecutorService是AutoCloseable,try-with-resources关闭时等待任务终止。finally先取消未完成任务再shutdownNow,try退出确认资源关闭。只调用shutdown而立刻返回,不能证明线程已经结束。InterruptedException向调用者传播,不吞掉中断后继续等待;若某层不能传播,应恢复Thread.currentThread().interrupt()并退出当前工作。

文件Reader、HTTP响应体和池都有明确生命周期。由谁创建、谁关闭是常用规则,但可通过接口约定转移;方法接收借用Reader不应擅自close。关闭异常可能作为suppressed异常保留,诊断时不要只打印主异常message丢掉因果链。

字节码、堆、GC和JFR证据 ​

先确认java -version和javac -version来自同一JDK,再看classpath和主类;UnsupportedClassVersionError首先检查编译目标和运行版本。javap显示字节码和签名,不是源代码性能分析器。线程多而CPU低可能是阻塞,堆持续增长可能是对象被长期引用,不能把全部延迟都归为GC。

sh
javap -classpath build-ledger -c Ledger
java -Xms32m -Xmx64m -Xlog:gc -XX:StartFlightRecording=filename=ledger.jfr,settings=profile,dumponexit=true -cp build-ledger Ledger a:2 b:3
jfr summary ledger.jfr

命令会在当前目录生成JFR文件;使用JDK自带jfr读取。短程序的录制通常以环境及启动事件为主,可能没有足够CPU样本,不能从空采样推断热点不存在。GC日志显示堆回收活动,不等于进程RSS。长进程可通过jcmd PID Thread.print看线程状态、jcmd PID GC.heap_info看堆;命令需同一用户和允许attach的环境,不要拿其他进程PID实验。

微基准需要预热、多个进程fork、结果消费和固定输入;本项目仅验证诊断采集链,不以一次CLI启动耗时比较HashMap与ConcurrentHashMap。实际性能比较应另用OpenJDK JMH,明确JDK、GC、堆、硬件和误差,避免JIT消除无结果工作。

练习与参考 ​

  1. 用可变List作为record键组件,插入Map后修改它,解释为什么可能查不到。
  2. 把累计移入工作线程并直接共享HashMap,指出需要保护的操作边界。
  3. 将解析换成慢I/O,设计超时与中断测试,不能依赖任意sleep等待。
解答与验证要求
  1. record生成的hashCode依据组件当前内容;内容变化后计算哈希和插入时不同。应在构造器中List.copyOf并保证元素不可变,或选稳定独立标识。
  2. get+加法+put整体属于复合更新,不是分别给get/put加锁。可用同一锁覆盖事务,或ConcurrentHashMap.merge并把纯计算放回调;本项目的单所有者更容易推理。
  3. 用CountDownLatch确保任务真正开始,再触发取消;回调捕获中断后释放资源并返回,测试用有界get/await作看门狗并确认活跃任务数归零。I/O层另配连接/读取超时,不能假设Future.cancel能取消所有阻塞API。

核对来源与旧材料取舍 ​

2026-10-17核对Java21 Map契约、ExecutorService内存及关闭语义和Dev.java学习入口。国内中文检索核读Microsoft官方Java JFR性能分析说明,仅用于理解采集和分析职责;本项目不依赖Azure,也不把云端分析器选项混入本地JDK命令。

原Java基础的类型/对象/集合练习与TSV项目仍保留,命令手册仍按操作场景可达。旧JDK8截图、占位Maven镜像与早期Spring Boot IDE创建步骤不再作为当前安装教程;旧URL保留明确说明及对应去向。SSM+Redis和Web面试旧记录包含独有框架/SQL内容,保持可达,不把其内容冒充本篇JVM课程或强行迁移到无关章节。

继续阅读