Java 25 “thân thiện với người mới”??
🚀 Những đột phá giải quyết các vấn đề nhức nhối
Java 25 không tập trung vào việc thêm thắt những tính năng mới, mà tập trung vào hoàn thiện hàng loạt project từ các phiên bản trước, giúp các nhà phát triển giải quyết các vấn đề thực tiễn.
1. Đập tan rào cản với người mới cùng Project Amber
Một trong những lời phàn nàn lớn nhất về Java là sự rườm rà của nó đối với người mới bắt đầu.
Vấn đề cũ: Để viết một chương trình "Hello, World!" đơn giản, một lập trình viên mới phải "nhai" qua một mớ cú pháp đáng sợ:

Giải pháp của Java 25 (JEP 512: Compact Source Files & Instance Main Methods)Java 25 cho phép các chương trình đơn giản được viết một cách ngắn gọn hơn, thậm chí không cần một class bao bên ngoài. Điều này giúp Java trở nên thân thiện hơn cho việc học và viết script nhanh.

Ngoài ra project Amber còn giải quyết một vấn đề nhức nhối lâu năm là câu lệnh super() hoặc this() luôn phải đứng tại vị trí đầu tiên trong constructor.
- Vấn đề cũ: Hàm super() luôn phải gọi đầu tiên trong constructor. Không thể validate parameter trước khi gọi super()

- Giải pháp của Java 25 (JEP 513: Flexible Constructor Bodies)Java 25 cho phép cho phép các câu lệnh chạy trước lệnh gọi super(), miễn là không sử dụng this phía trước super() ví dụ như this.age

2. Tối ưu hóa hiệu năng và bộ nhớ cùng Projects Lilliput & GCs
- Định kiến "Java ngốn bộ nhớ".Vấn đề cũ: Trên các máy 64-bit, mỗi object trong Java đều mang một "header" khá lớn (12-16 bytes), gây lãng phí bộ nhớ đáng kể khi có hàng triệu đối tượng.Giải pháp JEP 519: Compact Object Headers (Project Lilliput)Tính năng này được hoàn thiện, giảm kích thước header của đối tượng xuống còn 8 bytes. Đây là một tác động cực lớn: giảm đáng kể mức tiêu thụ heap từ 10-20%, điều này có nghĩa là ban sẽ tốn ít RAM hơn, tốn ít tiền hơn cho AWS. Chỉ cần thêm flag -XX:+UseCompactObjectHeaders vào VM option lúc chạy là được.
- Định kiến "Java chạy chậm lúc khởi động".Vấn đề cũ: Khởi động Java cần thời gian warm-up để trình biên dịch JIT (Just-In-Time) phân tích và tối ưu hóa code, khiến các ứng dụng khởi động chậm.Giải pháp JEP 514 & 515 Cải tiến AOT (Ahead-of-Time)Giải pháp cho vấn đề khởi động chậm là tạo ra AOT cache nhằm dịch chuyển các công việc nặng nề từ lúc runtime về lúc build time.
- JEP 514: Ahead-of-Time Command-Line ErgonomicsJEP này tập trung vào việc cải thiện trải nghiệm của lập trình viên bằng cách đơn giản hóa quy trình tạo AOT cache.
- Vấn đề trước đây:Để tạo AOT cache, anh em phải chạy lệnh java hai lần:
- Một lần ở AOTMode=record để thực hiện tạo ra tệp .aotconf.
- Một lần nữa ở AOTMode=create để đọc tệp .aotconf đó và tạo ra AOT cache (tệp .aot).
- Giải pháp của JEP 514:JEP này giới thiệu một tùy chọn mới là -XX:AOTCacheOutput=app.aot. Giờ đây, anh em chỉ cần chạy lệnh java một lần duy nhất. JVM sẽ tự động thực hiện cả hai bước (record và create) ngầm định, và tự xóa tệp cấu hình tạm thời.
- Vấn đề trước đây:Để tạo AOT cache, anh em phải chạy lệnh java hai lần:
- JEP 515: Ahead-of-Time Method ProfilingLàm cho AOT cache đó trở nên mạnh mẽ hơn bằng cách thêm các thông tin profile vào cache, giúp loại bỏ giai đoạn warm-up chậm chạp.
- Vấn đề trước đây:
- Java cần JIT compiler để đạt peak performance.
- Mỗi lần chạy ứng dụng thì JIT đều sẽ mất một lúc để thực hiện profile (thu thập các thông tin), nhằm xác định xem đâu là các hot method (các phương thức được gọi nhiều nhất) để tối ưu hóa.
- Khoảng thời gian thực hiện profile ban đầu này được gọi là warm-up, và trong suốt thời gian này, ứng dụng sẽ chạy chậm.
- Giải pháp của JEP 515: JEP này giải quyết vấn đề bằng cách thu thập các profile ngay trong lúc tạo AOT cache và lưu thẳng vào nó. Sau này ứng dụng khởi động cùng AOT cache thì JIT compiler sẽ có thể biết ngay lập tức đâu là các hot method và thực hiện tối ưu bỏ quả được bước thu thập profile chậm chạp.
- Vấn đề trước đây:
- JEP 514: Ahead-of-Time Command-Line ErgonomicsJEP này tập trung vào việc cải thiện trải nghiệm của lập trình viên bằng cách đơn giản hóa quy trình tạo AOT cache.
JEP 521: Generational ShenandoahJEP này công bố rằng chế độ generational của Shenandoah Garbage Collector đã chính thức production ready trong JDK 25, không còn là một tính năng thử nghiệm nữa.Về cơ bản thì chế độ này chia heap thành các "thế hệ" (thường là Young và Old). Với giả thuyết là hầu hết các object được tạo ra sẽ "chết" rất nhanh. Bằng cách chỉ tập trung thu dọn rác ở vùng "Young” thay vì trên toàn bộ heap, GC có thể hoạt động rất nhanh và hiệu quả, giúp cải thiện throughput và latency của ứng dụng.

3. Cách mạng hóa lập trình đa luồng cùng Project Loom
Đây có lẽ là thay đổi sâu sắc nhất, giải quyết sự phức tạp của lập trình đa luồng.
- Vấn đề cũ:Hãy tưởng tượng API requestHandler() của anh em nhận một request.
- requestHandler() sau khi nhận request sẽ có một request context (chứa ID người dùng, ID giao dịch, v.v.).
- requestHandler() gọi serviceHandler().
- serviceHandler() gọi repositoryHandler() một phương để đọc dữ liệu từ database.
- repositoryHandler() cần ID của người dùng trong request context ở bước 1.
Câu hỏi là làm thế nào để ae lấy được ID từ bước a để sử dụng ở bước d.
Anh em sẽ có 2 cách:
- Cách 1: là truyền qua method parameter như đa số hay làm.
- Cách 2: là sử dụng ThreadLocal nó như một cái ngăn đựng đồ được cấp riêng cho mỗi thread vậy. Lúc requestHandler() được gọi thì nhét ID vào, lúc repositoryHandler() cần ID thì lấy ra. Rất tiện mà không cần phải truyền userId qua từng method đúng không?

Thế nhưng ThreadLocal có những nhược điểm:
- Dễ bị thay đổi: Giá trị có thể được set tại bất kỳ hàm nào nên có thể bị thay đổi tại đâu đó trong thread mà ta không biết được. Nhất là khi mỗi ông code một đoạn =))
- Memory leaks: Giá trị được set sẽ tồn tại vĩnh viễn cho tới khi được gọi remove. Nếu quên gọi remove thì sẽ gây ra memory leak.
- Kế thừa tốn kém: Nếu dùng InheritableThreadLocal để thread con thấy dữ liệu của thread cha, Java sẽ copy toàn bộ dữ liệu. Với sự ra đời của Virtual Threads, chúng ta có thể tạo ra hàng triệu virtual thread. Việc sao chép dữ liệu cho hàng triệu thread sẽ gây tốn bộ nhớ nghiêm trọng.
Giải pháp JEP 506: Scoped Values
Chính thức hóa API Scoped Values. Đây là sự thay thế hiện đại cho ThreadLocal, có độ tương thích cao với virtual thread.

Scoped Values được thiết kế để chia sẻ immutable data một cách an toàn và hiệu quả giữa các phương thức trong cùng một luồng, cũng như với các luồng con.
Nó có các ưu điểm như sau:
- Immutable: ScopedValue không có phương thức set(). Giá trị CONTEXT được cố định tại thời điểm bắt đầu phạm vi của serviceHandler(), đảm bảo rằng giá trị này không thể bị thay đổi từ bên trong.
Mặc dù serviceHandler() có thể rebind một giá trị CONTEXT mới, giá trị mới này chỉ áp dụng cho các phương thức con mà nó gọi. Ngay cả khi rebind xảy ra, giá trị CONTEXT của chính serviceHandler() và của phương thức đã gọi nó vẫn được bảo toàn nguyên vẹn.
Do đó, nếu không rebind, một phương thức con như repositoryHandler() sẽ tự động kế thừa giá trị "old userId" từ serviceHandler(). - Bounded Lifetime: Giá trị của CONTEXT chỉ tồn tại trong thời gian thực thi của method run(). Ngay khi khối mã kết thúc (dù là kết thúc bình thường hay do ném ngoại lệ), giá trị ràng buộc sẽ tự động bị hủy. Không cần remove(), loại bỏ hoàn toàn nguy cơ rò rỉ bộ nhớ.
- Kế thừa hiệu quả: Đây là điểm "ăn tiền" khi dùng với virtual thread. Khi kết hợp với StructuredTaskScope (JEP 505), các luồng ảo con được fork() sẽ share các ScopedValue của thread cha một cách hiệu quả. Không có sự copy dữ liệu như InheritableThreadLocal, giúp tiết kiệm bộ nhớ cực lớn khi chạy hàng triệu luồng.
- Vấn đề cũ:Hãy tưởng tượng bạn có một tác vụ handleRequest() cần thực hiện hai công việc độc lập cùng lúc ví dụ như findUser() và fetchOrder() rồi gộp kết quả.

Với cách làm cũ như hình 1 ta sẽ có những vấn đề sau
- Thread Leaks: Nếu fetchOrder() thất bại, lệnh orderFuture.get() sẽ ném ngoại lệ. Tuy nhiên, thread chạy findUser() sẽ có thể vẫn đang chạy (nếu nó chưa chạy xong) mà không bị shutdown luôn, điều này dẫn đến lãng phí tài nguyên.
- Khó khăn trong việc cancel thread: Nếu thread chạy handleRequest() bị interrupt, không có gì đảm bảo rằng các thread con findUser() và fetchOrder() sẽ tự động bị ngắt theo. Chúng trở thành các thread "mồ côi" và cứ tiếp tục chạy gây lãng phí tài nguyên.
- Xử lý lỗi phức tạp: Nếu fetchOrder() chạy nhanh (1 giây) và xảy ra lỗi sớm nhưng findUser() chạy rất chậm (10 giây), handleRequest() vẫn phải chờ findUser() hoàn thành vì ta đang gọi userFuture.get() xong rồi mới gọi tới orderFuture.get() và phát hiện ra cái lỗi mà đánh nhẽ phải phát hiện từ 9 giây trước. Điều này gây lãng phí thời gian chờ đợi.
Giải pháp JEP 505: Structured Concurrency (Preview lần thứ 5)
Mặc dù vẫn là bản preview, nó đang tiến rất gần đến việc hoàn thiện.
Mục đích cốt lõi của JEP này là đơn giản hóa việc lập trình đa luồng bằng cách coi một nhóm các subtasks có liên quan, chạy trên các thread khác nhau, như một khối duy nhất.
Nó cung cấp API StructuredTaskScope để quản lý các tác vụ đồng thời như một khối duy nhất.

Structured Concurrency có một nguyên tắc đơn giản: "Nếu một parent task chia thành các subtask chạy đồng thời, tác vụ cha sẽ bị block và phải đợi cho đến khi tất cả các tác vụ con mà nó sinh ra đã hoàn thành (dù là thành công, thất bại hay bị hủy).".
Giải pháp này mang lại các lợi ích rõ rệt:
- Short-circuiting error handling: Nếu findUser() hoặc fetchOrder() thất bại, scope sẽ ngay lập tức cancel các task còn lại. Lệnh join() sẽ ném ra ngoại lệ, và bạn không phải chờ task chạy chậm hơn hoàn thành.
- Cancel các subtask: Nếu luồng chạy handleRequest() bị interrupt thì scope sẽ tự động hủy cả hai subtask findUser() và fetchOrder().
- Ràng buộc scope: Không tác vụ con nào có thể "sống sót" ra khỏi khối try-with-resources. Điều này loại bỏ hoàn toàn rủi ro thread leaks.
- Dễ debug: Các công cụ như thread dump (với jcmd) có thể hiển thị rõ cấu trúc cây, cho thấy luồng findUser() và fetchOrder() là con của luồng handleRequest(), giúp debug dễ dàng hơn.
Structured Concurrency cung cấp một giải pháp tốt hơn, an toàn hơn cho trường hợp pattern "fan-out/fan-in” thay vì mớ hỗn độn của CompletableFuture. Đặc biệt là khi anh em dùng virtual thread.
🤔 Những vấn đề còn tồn đọng
Dù Java 25 là một bước tiến lớn, nó vẫn chưa phải là vạch đích cuối cùng.
- Project Valhalla: JEP 507: Primitive Types in Patterns và JEP 502: Stable Values là những bước đệm, giấc mơ lớn về Value Types vẫn chưa hoàn thiện. Đây là một trong những cải tiến về hiệu năng mà cộng đồng vẫn đang chờ đợi.
- Project Panama: JEP 508: Vector API đã đi đến bản "Incubator" thứ 10. Điều này cho thấy việc tương tác hiệu quả với code non-Java (C/C++) và tận dụng các tập lệnh SIMD của CPU vẫn đang được tinh chỉnh rất kỹ lưỡng tuy nhiên chưa sẵn sàng cho môi trường production.
- Hệ sinh thái tích hợp: Java vẫn phụ thuộc rất nhiều vào các công cụ bên thứ ba cho những tác vụ cơ bản mà các ngôn ngữ mới (như Go, Rust) đã tích hợp sẵn. Java 25 vẫn không có:
- Một build tool chính thức, đồng nhất (vẫn là Maven/Gradle).
- Một thư viện xử lý JSON/XML tích hợp sẵn, hiện đại (vẫn là Jackson/Gson).
- Một code formatter chính thức.
Anh em nghĩ sao về đợt hoàn thiện này của Java?
Liệu đây có phải là bước đột phá giúp Java "thân thiện" hơn, dễ trở thành lựa chọn cho người mới?
🤔 Hay với newbie, Java vẫn là một "ngọn núi" và họ sẽ "quay xe" chọn Python/JS cho nó "dễ thở"?