Một trăm bài, một trăm ngày. Bài này nhìn lại chặng đường và vẽ bản đồ đi tiếp.

Bảy chặng

Bài 1–10: nền tảng. Từ cài JDK tới biến, kiểu, vòng lặp, String, mảng, package và classpath.

Bài 11–25: hướng đối tượng. Lớp, kế thừa, đa hình, interface, hợp đồng equals/hashCode, record, sealed, pattern matching, và SOLID qua ví dụ thật.

Bài 26–45: thư viện chuẩn. Ngoại lệ, Optional, toàn bộ Collections Framework, generics và quy tắc PECS, xoá kiểu, cách chọn cấu trúc dữ liệu theo độ phức tạp.

Bài 46–66: Java hiện đại. Lambda, Stream, CompletableFuture sơ khởi, I/O với java.nio.file, bảng mã, serialization, JSON, java.time và múi giờ.

Bài 67–80: đồng thời. Thread, synchronized, mô hình bộ nhớ, CAS, khoá, deadlock, BlockingQueue, ExecutorService, ForkJoinPool, bộ sưu tập an toàn luồng, ThreadLocal, luồng ảo, structured concurrency.

Bài 81–90: bên trong JVM. Class loader, bố trí bộ nhớ, bytecode, JIT, các bộ thu gom rác, đọc GC log, heap dump, JFR, JMH.

Bài 91–100: công cụ. Maven, Gradle, JUnit 5, Mockito, Testcontainers, test đồng thời, Docker, log và giám sát, bảo mật.

Mười lần phép đo nói ngược

Đây là phần tôi thấy giá trị nhất của cả sê-ri, và cũng là thứ tôi không lường trước khi bắt đầu. Mỗi lần ngồi đo một điều "ai cũng biết", có một tỷ lệ đáng kể nó không đúng như vậy.

Regex (a+)+b không còn treo được JDK 21 (bài 63). Mười lăm mẫu ReDoS kinh điển, mẫu chậm nhất là 1,8 ms. Thứ thật sự treo được là sáu nhóm .* liền nhau — 19 giây cho chuỗi 120 ký tự.

Luồng ảo không chậm hơn ở tải tốn CPU (bài 79). 89 ms so với 90 ms của pool nền tảng. Lời khuyên "đừng dùng luồng ảo cho tải CPU" đúng kết luận nhưng sai lý do: không phải vì chậm, mà vì chẳng được gì.

AtomicLong thua synchronized khi tranh chấp cao (bài 70). Ở 32 luồng: 347 ms so với 251 ms. Không khoá không phải lúc nào cũng nhanh hơn khoá.

ReadWriteLock có lúc chậm hơn khoá thường (bài 71). Với thao tác đọc ~1 µs, nó chậm hơn 1,4 lần. Chỉ khi giữ khoá đọc đủ lâu nó mới thắng.

Khoá công bằng chậm hơn 2.600 lần (bài 71). 337 triệu lượt so với 127 nghìn trong hai giây. Bật "cho chắc" là cắt thông lượng đi ba bậc độ lớn.

Maven build lại chậm hơn chính build sạch của nó (bài 92). 1280 ms so với 950 ms, ổn định qua ba lần.

H2 không nhanh hơn PostgreSQL (bài 95). Chèn hàng loạt thì PostgreSQL nhanh gấp đôi. Cái đắt của Testcontainers là 680 ms khởi động, không phải tốc độ thao tác.

Ảnh Docker nhỏ không khởi động nhanh hơn (bài 97). Ảnh 126 MB khởi động chậm hơn ảnh 285 MB.

log.debug("{}", x) vẫn tốn tiền khi DEBUG tắt (bài 98). 37 ms cho hai triệu lần gọi, vì đối số vẫn được tính. Cách này được dạy như cách chữa mà chỉ chữa một nửa.

Ba trong bốn cái bẫy microbenchmark không xảy ra (bài 90). Và tệ hơn: từ phép đo tự viết, tôi đã suýt kết luận "JIT gấp hằng số" cho một chênh lệch thật ra đến từ một phép chia lấy dư.

Bài học rút ra không phải "đừng tin ai". Nó là: lời khuyên hiệu năng có hạn sử dụng. Phần lớn những câu trên từng đúng — với JDK 8, với máy bốn nhân, với ổ cứng quay. Chúng được lặp lại tới khi thành sự thật hiển nhiên, trong khi nền tảng bên dưới đã đổi.

Cách phòng: đo trên chính tải của bạn, trên chính phiên bản bạn chạy.

Ba thói quen đáng giữ

Đo trước khi tối ưu, và đo lại sau khi tối ưu. Bài 89 và 90 là bộ công cụ đầy đủ cho việc đó, và cả hai đều có sẵn trong JDK.

Đọc thông báo lỗi tới dòng cuối. NoClassDefFoundError: Could not initialize class X không phải thiếu jar (bài 81). class Rieng cannot be cast to class Rieng hoàn toàn có lý khi biết danh tính lớp là cặp tên-và-loader. Java hiếm khi nói dối; nó chỉ nói bằng thứ ngôn ngữ cần học.

Đừng viết thứ đã có người viết. BlockingQueue thay wait/notify, ConcurrentHashMap thay tự khoá, ExecutorService thay tự quản lý luồng. Bài 96 cho thấy vì sao: bạn không test được mã đồng thời tới mức chắc chắn, nên hãy dùng thứ đã được người khác test bằng jcstress.

Đi tiếp từ đây

Sê-ri này dừng ở nền tảng và JVM. Bảy nhánh đi tiếp, tuỳ hướng bạn chọn:

Web và dịch vụ — Spring Boot là mặc định của thị trường Việt Nam. Học theo thứ tự: tiêm phụ thuộc, Spring MVC, Spring Data JPA, Spring Security. Quarkus và Micronaut đáng biết khi cần khởi động nhanh và ảnh nhỏ.

Dữ liệu — SQL cho thật giỏi trước khi học ORM. Rồi kế hoạch thực thi truy vấn, chỉ mục, mức cô lập giao dịch. Bài 65 và 66 mới chạm phần vỏ.

Kiến trúc — Domain-Driven Design, kiến trúc lục giác, và các mẫu tích hợp. Đây là chỗ những bài về SOLID và sealed trả công.

Hiệu năng sâu — JMH nghiêm túc, đọc mã assembly do JIT sinh, hiểu tầng bộ nhớ đệm CPU. Nhánh này ít người đi và rất đáng giá.

Đồng thời nâng cao — cấu trúc dữ liệu không khoá, jcstress, và các mô hình khác như actor.

Ngôn ngữ khác trên JVM — Kotlin nếu làm Android hoặc muốn cú pháp gọn hơn; Scala nếu làm dữ liệu lớn; Clojure nếu muốn đổi hẳn cách nghĩ.

Vận hành — Kubernetes, khả năng quan sát, kỹ thuật độ tin cậy. Bài 97, 98 là điểm khởi đầu.

Sách và tài liệu

Bốn cuốn tôi thật sự dùng, không phải danh sách cho đẹp:

Effective Java (Joshua Bloch) — cuốn duy nhất tôi nói là bắt buộc. Đọc lại mỗi năm sẽ hiểu khác đi.

Java Concurrency in Practice (Brian Goetz) — cũ hơn mười lăm năm nhưng phần mô hình bộ nhớ và nguyên tắc thiết kế vẫn đúng nguyên. Đọc cùng chặng 67–80.

Optimizing Java (Evans, Gough, Newland) — về JIT, GC và đo hiệu năng. Nối tiếp chặng 81–90.

Clean Architecture (Robert C. Martin) — đọc có chọn lọc, lấy phần ranh giới và phụ thuộc.

Nguồn theo dõi thường xuyên: JEP index trên openjdk.org cho biết Java sắp có gì; Inside Java cho bài viết từ chính nhóm phát triển; và thông báo phát hành của JDK — mỗi sáu tháng một bản, đọc mất mười lăm phút và giúp bạn không tụt lại.

Điều cuối

Một trăm bài này viết theo một nguyên tắc duy nhất: không viết điều mình chưa chạy thử.

Nó tốn thời gian hơn nhiều so với việc chép lại kiến thức có sẵn, và nó khiến tôi phải sửa hoặc bỏ hẳn nhiều đoạn đã viết xong. Nhưng nó cũng là lý do sê-ri có mười trường hợp ở trên — những chỗ mà nếu chỉ đọc tài liệu rồi viết lại, tôi đã truyền tiếp một điều không còn đúng.

Nếu có một thứ đáng mang theo từ một trăm ngày này, tôi nghĩ là thứ đó: chạy thử đi, rồi hãy tin.

Cảm ơn bạn đã đọc tới đây.