ข้าม​ไป​ยัง​เนื้อหา

capstone — เปิด HTTP ด้วย JDK เปล่า แล้ว​ทำงาน​พร้อม​กัน​ด้วย virtual thread

เจ็ด​บท​ที่​ผ่าน​มา orderctl ทำงาน​ที​ละ​ใบ — อ่า​นอ​อเด​อร์​จาก JSON ตรวจ​ความ​ถูก​ต้อง คิด​เงิน เขียน​สถานะ​ลง disk แล้ว​จบ

บท​นี้​เปลี่ยน​สอง​อย่าง​พร้อม​กัน มัน​เปิด port ให้​คน​อื่น​ยิง​เข้า​มา และ​มัน​คิด​เงิน​หลาย​ใบ​พร้อม​กัน​ได้

📦 สิ่ง​ที่​บท​นี้​เพิ่ม​ให้ orderctl
  • Quote กับ Quotes.quote — ใบ​เสนอ​ราคา และ​ตรรกะ​ของ POST /quote ที่​ไม่รู้​จัก HTTP เลย
  • QuoteHandler — ตัว​รับคำ​ขอ​ที่​ปิด​กับ​ดัก​สี่​ข้อ​ของ com.sun.net.httpserver
  • Serve — เปิด port แล้ว​ผูก executor ที่​แจก virtual thread ให้​ทุก​คำขอ
  • Batch กับ Ledger — คิด​เงิน​หลาย​ใบ​พร้อม​กัน และ​ยอด​สะสม​ที่​หลาย​สาย​เขียน​ได้

ฝั่ง HTTP ไม่​ต้อง​เพิ่ม dependency สัก​บรรทัด · แต่​ฝั่ง​เทสต์​ต้อง​เพิ่ม​เสมอ เพราะ JDK ไม่มี test framework มา​ให้ ซึ่ง​เป็น​ข้อ​ที่​คอร์ส Go ไม่​ต้อง​พูด​ถึง​เลย

JDK มี​เซิร์ฟเวอร์ HTTP ให้​แล้ว และ​มัน​เขียน​ขอบ​ของ​ตัว​เอง​ไว้

หัวข้อ​ที่​มีชื่อ​ว่า “JDK มี​เซิร์ฟเวอร์ HTTP ให้​แล้ว และ​มัน​เขียน​ขอบ​ของ​ตัว​เอง​ไว้”

ชื่อ package ขึ้น​ต้น​ด้วย com.sun แต่ JEP ยืนยัน​ว่า​รองรับ​จริง ขณะ​ที่​เอกสาร​ของ module เขียน​ข้อ​จำกัด​ไว้​เอง

com.sun.net.httpserver อยู่​ใน module jdk.httpserver ซึ่ง export ออก​มา​ตาม​ปกติ คุณ​จึง​เรียก​มัน​ได้​โดย​ไม่​ต้อง​เติม --add-exports และ​ไม่​ต้อง​แตะ pom.xml

ชื่อ​ที่​ขึ้น​ต้น​ด้วย com.sun เป็น​สัญญาณ​ของ internal API ใน​ที่​อื่น · ที่​นี่​ไม่ใช่ JEP 408 เขียน​ไว้​ว่า “The package is officially supported, and we extend it with APIs to simplify server creation and enhance request handling.”

แต่​เอกสาร​ของ module เดียวกัน​เขียน​ขอบ​ของ​ตัว​เอง​ไว้​สอง​ที่ และ​ผู้​อ่าน​ต้อง​เห็น​ทั้ง​สอง​ด้าน

  • API Note ของ module อธิบาย​ว่า API และ SPI ชุด​นี้​ออกแบบ​มา​เพื่อ​รองรับ​เซิร์ฟเวอร์ ขั้น​ต่ำ​และ​ความ​หมาย​พื้น​ฐาน​ของ HTTP เป็น​หลัก
  • Implementation Note ของ module เขียน​ตรง​ว่าการ​ออกแบบ​และ​การ​ทำ​จริง​ของ​เซิร์ฟเวอร์ “does not intend to be a full-featured, high performance HTTP server”
  • หน้า​สรุป​ของ package เขียน​ว่า “The API provides a partial implementation of RFC 2616 (HTTP 1.1) and RFC 2818 (HTTP over TLS).”

สาม​ข้อ​นี้​ไม่​ได้​แปล​ว่า​ห้าม​ใช้ มัน​แปล​ว่า​คุณ​ต้อง​รู้​ว่า​อะไร​ที่​มัน​ไม่​ทำให้ แล้ว​เขียน​ส่วน​นั้น​เอง ซึ่ง​เป็น​เนื้อหา​ของ​หัวข้อ​ถัด​ไป​ทั้ง​หัวข้อ

เมื่อไร​ควร​ย้าย​ไป Jetty, Javalin หรือ Helidon

ทาง​เลือก​ที่​ยัง​พัฒนา​อยู่​จริง​มี​หลาย​ตัว และ​ทุก​ตัว​เพิ่ม dependency เข้า​มา เส้น​แบ่ง​ที่​ใช้ได้​จริง​มี​สาม​ข้อ

  • คุณ​ต้องการ HTTP/2 — com.sun.net.httpserver พูด HTTP/1.1 อย่าง​เดียว และ JEP 517 ที่​เพิ่ม HTTP/3 ใน JDK 26 เขียน Non-Goal ไว้​เอง​ว่า​ไม่​ทำ​ฝั่ง​เซิร์ฟเวอร์
  • คุณ​ต้องการ TLS ที่​ตั้ง​ค่า​ง่าย — ที่​นี่​ตั้ง​ได้ แต่​ต้อง​ประกอบ HttpsConfigurator เอง
  • คุณ​ต้องการ routing ที่​ดู HTTP method ให้ — ที่​นี่​ไม่มี และ​คุณ​จะ​เห็น​ราคา​ของ​มัน​ใน​อีก​สอง​ย่อหน้า

ถ้า​ไม่​เข้า​สาม​ข้อ​นี้ ของ​ใน​กล่อง​ทำงาน​ได้​จริง และ​มัน​คือ​เหตุผล​ที่​บท​นี้​ไม่​เพิ่ม​อะไร​เลย

handler ตัว​แรก กับ​กับ​ดัก​สี่​ข้อ​ที่​ต้อง​ปิด​พร้อม​กัน

หัวข้อ​ที่​มีชื่อ​ว่า “handler ตัว​แรก กับ​กับ​ดัก​สี่​ข้อ​ที่​ต้อง​ปิด​พร้อม​กัน”

HttpHandler มี method เดียว แต่​ลำดับ​การ​เรียก หน่วย​ของ​ความ​ยาว ค่าที่​แปล​ว่า​ไม่มี body และ Content-Type ผิด​ได้ที​ละ​ข้อ

HttpHandlerHttpHandlerตัว​รับคำ​ขอ​ของ `com.sun.net.httpserver` · เส้น​ทาง​จับ​แบบ prefix ล้วน​และ​ไม่​ดู HTTP method ส่วน​ความ​ผิด​พลาด​ที่​หลุด​ออก​มา​ถูก​กลืน​เงียบ คือ interface ที่​มี method เดียว​คือ handle(HttpExchange) · ทุก​อย่าง​ที่​เกี่ยว​กับ​คำขอ​หนึ่ง​ใบ​อยู่​ใน HttpExchange ตัว​นั้น

package orderctl;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
/** ตัวรับคำขอของ POST /quote · ตรรกะทั้งหมดอยู่ในฟังก์ชันที่ส่งเข้ามาทาง constructor */
public final class QuoteHandler implements HttpHandler {
private final Function<String, String> logic;
public QuoteHandler(Function<String, String> logic) {
this.logic = logic;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
try (exchange) { // AutoCloseable ตั้งแต่ JDK 14
if (!"POST".equals(exchange.getRequestMethod())) { // routing ไม่ดู method ให้
exchange.sendResponseHeaders(405, -1); // -1 = ไม่มี body เลย (0 = chunked)
return;
}
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
int status;
String reply;
try {
reply = logic.apply(body);
status = 200;
} catch (RuntimeException e) { // ถ้าหลุดออกไป JDK จะกลืนเงียบ
reply = "{\"error\":\"bad order\"}";
status = 400;
}
byte[] out = reply.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(status, out.length); // จำนวนไบต์ ไม่ใช่ length() ของ string
try (OutputStream stream = exchange.getResponseBody()) {
stream.write(out);
}
}
}
}

สี่​บรรทัด​ที่​ดู​จุกจิก​ใน​นั้น​คือ​กับ​ดัก​สี่​ข้อ และ​ทุก​ข้อ​ล้ม​แบบ​ที่​อ่าน​ไม่​ออก​ว่า​เกิด​อะไร​ขึ้น

  1. sendResponseHeaders ต้อง​มา​ก่อน​แตะ stream — ตั้ง header หลัง​จาก​นั้น​ไม่มี​ผล​แล้ว
  2. ความ​ยาว​ที่​ส่ง​เข้าไป​เป็น​จำนวน​ไบต์ ไม่ใช่​จำนวน​ตัว​อักษร
  3. -1 แปล​ว่า​ไม่มี body ส่วน 0 แปล​ว่า chunked · ตอบ 405 ด้วย 0 แล้ว​ลืม​ปิด stream ทำให้ client ค้าง​จน timeout แทนที่​จะ​ได้​คำ​ตอบ​ทันที
  4. Content-Type ต้อง​ตั้ง​เอง JDK ไม่​เดา​ให้ · ไม่​ตั้ง​แล้ว client ที่​อ่าน header ตัว​นี้​จะ​เดา​ผิด และ​ภาษา​ไทย​ใน body จะ​เพี้ยน​ที่​ปลาย​ทาง

ตรรกะ​จริง​ไม่​ได้​อยู่​ใน​นี้​เลย มัน​ถูก​ส่ง​เข้า​มา​ทาง constructor เป็น Function<String, String> ซึ่ง​เป็นการ​ตัดสิน​ใจ​ที่​จะ​ได้​ผล​ตอบแทน​คืน​ใน​หัวข้อ​เทสต์

// src/main/java/orderctl/Quote.java — ชนิดใหม่ตัวเดียวที่บทนี้เพิ่ม
package orderctl;
public record Quote(String id, long totalSatang) { }

ตรรกะ​ของ POST /quote อยู่​ใน file ของ​ตัว​เอง · Order กับ Line มา​จาก​บท​ที่ 3 ส่วน mapper เป็น​ตัว​เดียว​กับ​ที่​บท​ที่ 3 ตั้ง​ไว้

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี Order.java กับ Quote.java อยู่ข้างกัน
// src/main/java/orderctl/Quotes.java — ตรรกะของ POST /quote
package orderctl;
import tools.jackson.databind.json.JsonMapper;
public final class Quotes {
private static final JsonMapper MAPPER = JsonMapper.builder().build();
private Quotes() {
}
public static String quote(String body) {
Order order = MAPPER.readValue(body, Order.class);
return MAPPER.writeValueAsString(new Quote(order.id(), order.grossSatang()));
}
}

ยอด​รวม​ไม่​ได้​ถูก​คิด​ใหม่​ใน​นี้ · order.grossSatang() กับ line.subtotalSatang() เขียน​ไว้​แล้ว​ตั้งแต่​บท​ที่ 2 กับ​บท​ที่ 3 และ​บท​นี้​เรียก​ใช้​ของ​เดิม

catch (RuntimeException e) ใน handler ครอบ​ทั้ง​ความ​ผิด​พลาด​ของ​โดเมน​และ​ของ JSON ได้​พอดี เพราะ Jackson 3 เปลี่ยน​ความ​ผิด​พลาด​ของ​ตัว​เอง​เป็น unchecked หมด​แล้ว

JSON ที่​ขาด​กลาง​คัน​จึง​มา​ถึง​ใน​รูป tools.jackson.core.exc.UnexpectedEndOfInputException ซึ่ง​เป็น RuntimeException · ใน Jackson 2 มัน​เป็น checked และ​ต้อง​ประกาศ throws ขึ้น​ไป​ทั้ง​สาย

ข้อ​นี้​เป็น​บั๊ก​ที่​ตัวอย่าง​ภาษา​อังกฤษ​ล้วน​ไม่มี​วัน​เจอ เพราะ​สอง​เลข​นั้น​เท่า​กัน​พอดี​เมื่อ​ทุก​ตัว​อักษร​เป็น ASCII

package orderctl;
import java.nio.charset.StandardCharsets;
public final class ByteCount {
public static void main(String[] args) {
String sku = "ชาไทยเย็น";
System.out.println(sku.length());
System.out.println(sku.getBytes(StandardCharsets.UTF_8).length);
}
}
$ mvn -q compile exec:java -Dexec.mainClass=orderctl.ByteCount
9
27

ส่ง​เลข 9 ไป​ให้ sendResponseHeaders ทั้ง​ที่​ต้อง​เขียน 27 ไบต์ แล้ว​ผล​ไม่ใช่ body ที่​ขาด​หาย

ผล​คือ สาย​ขาด · เซิร์ฟเวอร์​ล้ม​ด้วย java.io.IOException: too many bytes to write to stream แล้ว​ปิด connection ทิ้ง · อ่าน​ที่ socket ตรงๆ แล้ว​นับ​ได้ 0 ไบต์ ทั้ง header และ body จึง​ไม่มี​อะไร​ออก​ไป​ถึง​ผู้​เรียก​เลย​สัก​บรรทัด

client ที่​ใช้ java.net.http.HttpClient จึง​ได้ java.io.IOException: HTTP/1.1 header parser received no bytes ซึ่ง​ไม่มี​คำ​ว่า​ไบต์​ผิด​อยู่​ใน​นั้น​เลย

server.createContext("/quote", handler) ไม่​ได้​แปล​ว่า​เส้น​ทาง​นี้​เท่านั้น มัน​แปล​ว่า ทุก​เส้น​ทาง​ที่​ขึ้น​ต้น​ด้วย​ข้อความ​นี้ และ​ไม่​ว่า method อะไร​ก็​เข้า​ตัว​เดียวกัน

วัด​จาก​เซิร์ฟเวอร์​ที่​หัวข้อ​นี้​เขียน​ขึ้น​จริง โดย​แนบ body ที่​ถูก​ต้อง​ไป​ทุก​คำขอ บน OpenJDK 25.0.4.1 macOS arm64 เมื่อ 2026-08-27

คำขอได้​อะไรใคร​เป็น​คน​ตัดสิน
POST /quote200routing ส่ง​เข้า handler
POST /quotes200routing ส่ง​เข้า handler ตัว​เดียวกัน เพราะ​จับ​แบบ prefix
POST /quote/A-1200เหมือน​กัน · path ที่​เหลือ​เป็น​เรื่อง​ของ​คุณ​เอง
GET /quote405บรรทัด​ที่​คุณ​เขียน​เอง ไม่ใช่ routing
DELETE /quote405บรรทัด​เดียวกัน​นั้น

ถอด if (!"POST".equals(...)) ออก แล้ว DELETE /quote ที่​แนบ body ถูก​ต้อง​มา​ด้วย จะ​ได้ 200 พร้อม​ใบ​เสนอ​ราคา · routing ไม่​เคย​เป็น​คน​ห้าม

Exchange.run() จับ Exception แล้ว log ที่​ระดับ​ที่​ปิด​อยู่ หน้า​จอ​เซิร์ฟเวอร์​จึง​ว่าง​เปล่า

โยน IllegalStateException ออก​จาก handle แล้ว​ดู​หน้า​จอ​เซิร์ฟเวอร์ · ไม่มี​อะไร​ขึ้น​เลย ส่วน client ได้ java.io.IOException: HTTP/1.1 header parser received no bytes ตัว​เดิม

กลไก​อยู่​ใน sun.net.httpserver.ServerImpl$Exchange.run() ซึ่ง​จับ Exception แล้ว​เขียน log ที่​ระดับ FINER · ระดับ​นั้น​ปิด​อยู่​โดย​ปริยาย ทุก​อย่าง​จึง​หาย​ไป

เปิด​มัน​ขึ้น​มา​ชั่วคราว​ได้​ด้วย​การ​ตั้ง​ค่า logging ของ JDK

handlers=java.util.logging.ConsoleHandler
com.sun.net.httpserver.level=ALL
java.util.logging.ConsoleHandler.level=ALL

แล้ว​รัน​เซิร์ฟเวอร์​ตัว​เดิม​อีก​ครั้ง​โดย​เติม -Djava.util.logging.config.file=log.properties เข้าไป แล้ว​ยิง​คำขอ​ที่​ทำให้ handler ระเบิด

Aug 27, 2026 3:33:55 PM sun.net.httpserver.ServerImpl$Exchange run
FINER: ServerImpl.Exchange
java.lang.IllegalStateException: โยนออกมาเลย
at orderctl.QuoteHandler.handle(QuoteHandler.java:28)
[... ห้าบรรทัดของ Filter$Chain, AuthFilter และ LinkHandler ...]
at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:900)
at java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:291)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:460)

บรรทัด​บน​สุด​คือ file ของ​คุณ​เอง เลข​บรรทัด​จึง​ไม่​ตรง​กับ​ข้าง​บน​นี้ · สาม​บรรทัด​ล่าง​คือ​ของ JDK และ​มัน​บอก​ครบ​ว่า​ใคร​เป็น​คน​กลืน

นี่​คือ​เครื่อง​มือ debug ไม่ใช่​ทาง​แก้ · ทาง​แก้​จริง​คือ​จับ​ความ​ผิด​พลาด​ไว้​ใน handler เอง​เสมอ อย่าง​ที่ QuoteHandler จับ RuntimeException แล้ว​แปลง​เป็น​สถานะ HTTP ที่​ผู้​เรียก​อ่าน​ออก

`Throwable` ที่​ไม่ใช่ `Exception` ไม่​ถูก​กลืน

Exchange.run() จับ​เฉพาะ Exception · OutOfMemoryError หรือ StackOverflowError ถูก log แล้ว​โยน​ต่อ ไม่​ได้​ถูก​กลืน

แปล​ว่า​อาการ “หน้า​จอ​ว่าง​เปล่า” ที่​หัวข้อ​นี้​อธิบาย ครอบ​เฉพาะ​ความ​ผิด​พลาด​ของ code คุณ​เอง · ความ​ผิด​พลาด​ระดับ JVM ยัง​ส่ง​เสียง

เส้น​บน​สุด​ที่​ควร​จับ​จึง​เป็น Exception ส่วน catch (Throwable) ไม่ใช่ มัน​จะ​กลืน​ของ​ที่ JDK ตั้งใจ​ปล่อย​ให้​ดัง

ไม่​เรียก setExecutor แปล​ว่า​เสิร์ฟ​ที​ละ​คำขอ และ​เรียก​หลัง start แล้ว​โยน​ทิ้ง​ทันที

javadoc ของ HttpServer.setExecutor อธิบาย​ว่า​ถ้า​ไม่​เรียก​มัน​ก่อน start() หรือ​เรียก​ด้วย null เซิร์ฟเวอร์​จะ​ใช้​ตัว​ปริยาย​ซึ่ง​ทำงาน​บน thread ที่ start() สร้าง​ขึ้น

thread เดียว​หมาย​ถึง​คำขอ​ที่​สอง​รอ​คำขอ​แรก​จบ​ก่อน​เสมอ

บรรทัด​ที่​แก้​ข้อ​นั้น​แจก virtual threadvirtual threadthread ที่​ตัว​จัด​คิว​ของ JVM ดูแล​เอง ราคา​ถูก​จน​สร้าง​เป็น​ล้าน​ตัว​ได้ (final ที่ Java 21) · เป็น daemon และ​ไม่มี​ชื่อ​โดย​ปริยาย ให้​ทุก​คำขอ — thread ที่​ตัว​จัด​คิว​ของ JVM ดูแล​เอง ราคา​ถูก​จน​สร้าง​เป็น​ล้าน​ตัว​ได้ และ​เป็น​ของ​ที่​เสร็จ​สมบูรณ์​ที่ Java 21

package orderctl;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
/** เปิด port แล้วผูก handler เข้ากับเส้นทาง /quote */
public final class Serve {
private Serve() { }
public static HttpServer start(int port, HttpHandler handler) throws IOException {
// new InetSocketAddress(port) เฉยๆ เท่ากับ 0.0.0.0 คือเปิดให้ทั้งวง
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); // ขาดบรรทัดนี้ = ทีละคำขอ
server.createContext("/quote", handler);
server.start();
System.out.println("orderctl ฟังที่ http://localhost:" + server.getAddress().getPort());
return server;
}
}

start รับ handler เข้า​มา​แทนที่​จะ​สร้าง​เอง ตัว​ที่​ประกอบ​สอง​ชิ้น​เข้า​ด้วย​กัน​จึง​เป็น main

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · เติมลงใน Serve ข้างบน เพื่อให้ exec:java เรียกได้
public static void main(String[] args) throws IOException {
start(8080, new QuoteHandler(Quotes::quote));
}

ลำดับ​สี่​บรรทัด​นั้น​บังคับ — create แล้ว setExecutor แล้ว createContext แล้ว start

สลับ​สอง​ตัวกลาง​ได้ แต่​ย้าย setExecutor ไป​หลัง start() แล้ว​มัน​โยน java.lang.IllegalStateException: server already started ทันที

flowchart LR
  C1["คำขอที่ 1"] --> S{"executor<br/>ที่ผูกไว้"}
  C2["คำขอที่ 2"] --> S
  C8["คำขอที่ 8"] --> S
  S -->|"ไม่เรียก setExecutor"| T["thread เดียวที่ start สร้าง<br/>เสิร์ฟทีละใบ ต่อคิวกัน"]
  S -->|"newVirtualThreadPerTaskExecutor"| V["virtual thread ใบละตัว<br/>ทั้งแปดใบเดินพร้อมกัน"]
  T --> R1["เวลารวม ≈ ผลบวกของทุกใบ"]
  V --> R2["เวลารวม ≈ ใบที่ช้าที่สุด"]

คำ​บรรยาย​ภาพ: คำขอ​ทุก​ใบ​ผ่าน executor ตัว​เดียวกัน · เส้น​บน​คือ​ค่า​ปริยาย​ที่​มี thread เดียว เวลา​รวม​จึง​เป็น​ผล​บวก · เส้น​ล่าง​แจก virtual thread ให้​ทุก​ใบ เวลา​รวม​จึง​เท่ากับ​ใบ​ที่​ช้า​ที่สุด

วัด​บน OpenJDK 25.0.4.1 macOS arm64 (18 core) เมื่อ 2026-08-27 ด้วย handler ที่​หน่วง 300 ms ทุก​ใบ ยิง​จาก client ใน​โปร​เซ​ส​เดียวกัน

สิ่ง​ที่​วัดไม่มี setExecutorมี newVirtualThreadPerTaskExecutor
8 คำขอ≈2,590 ms≈315 ms
500 คำขอไม่​ได้​วัด≈394 ms

ตัวเลข​วินาที​จริง​ไม่​ข้าม​เครื่อง สิ่ง​ที่​ข้าม​ได้​คือ​รูป​ของ​เส้น · เส้น​แรก​เป็น​เส้น​ตรง​ตาม​จำนวน​คำขอ เส้น​ที่​สอง​แทบ​แบน​จาก 8 ถึง 500

virtual thread ไม่​ได้​แก้​ทุก​อย่าง

javadoc ของ Thread อธิบาย​ว่า virtual thread เหมาะ​กับ​งาน​ที่​ใช้​เวลา​ส่วน​ใหญ่​ไป​กับ​การ​บล็อก โดย​เฉพาะ​การ​รอ I/O และ​เขียน​ต่อว่า​มัน​ไม่​ได้​ตั้งใจ​ให้​ใช้​กับ​งาน​ที่​กิน CPU ยาวๆ

orderctl เข้า​เงื่อนไข​แรก​พอดี เพราะ​เวลา​ส่วน​ใหญ่​หมด​ไป​กับ​การ​รอ​สาย ไม่ใช่​การ​คำนวณ

อีก​สอง​ข้อ​ที่​กัด​ตอน debug — virtual thread ไม่มี​ชื่อ​โดย​ปริยาย และ​มัน​เป็น daemon เสมอ · thread dump จึง​เต็ม​ไป​ด้วย​รายการ​ที่​ไม่มี​ชื่อ และ​ไม่มี​ตัว​ไหน​กัน JVM ไม่​ให้​จบ

close() บล็อก​จน​งาน​จบ ส่วน execute กับ submit เก็บ​ข้อ​ยกเว้น​ไว้​คนละ​ที่

ExecutorServiceExecutorServiceตัว​รับ​งาน​ไป​ทำ​พร้อม​กัน · เป็น `AutoCloseable` ตั้งแต่ Java 19 และ `close()` บล็อก​จน​งาน​จบ จึง​ทำ​หน้าที่​เดียว​กับ WaitGroup ของ Go เป็น AutoCloseable ตั้งแต่ Java 19 และ close() ของ​มัน บล็อก​จน​งาน​ที่​ส่ง​เข้าไป​แล้ว​จบ​ครบ

นั่น​ทำให้ try (var pool = ...) ทำ​หน้าที่​เดียว​กับ wg.Wait() ของ Go แบบ​ตรง​ตัว · ต่าง​กัน​ที่ Go ต้อง​เรียก​เอง ส่วน Java ผูก​ไว้​กับ​วงเล็บ​ปีกกา

package orderctl;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.ToLongFunction;
/** คิดเงินหลายใบพร้อมกัน · close() ของ try-with-resources รอทุกงานจบ */
public final class Batch {
private Batch() { }
public static long chargeAll(List<String> ids, ToLongFunction<String> charge)
throws InterruptedException, ExecutionException {
List<Future<Long>> pending;
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
pending = ids.stream().map(id -> pool.submit(() -> charge.applyAsLong(id))).toList();
}
long sum = 0;
for (Future<Long> f : pending) {
sum += f.get(); // ต้องเรียก get() ไม่งั้นความผิดพลาดหายไปตลอดกาล
}
return sum;
}
}

บรรทัด f.get() ไม่ใช่​ของ​ประดับ · มัน​คือ​จุด​เดียว​ที่​ความ​ผิด​พลาด​ของ​งาน​แต่ละ​ใบ​โผล่​ออก​มา

วิธี​ส่ง​งานข้อ​ยกเว้น​ไป​อยู่​ที่ไหนคุณ​เห็น​มัน​เมื่อไร
execute(Runnable)ไป​ถึง uncaught exception handler ของ threadเห็น​ทันที​ที่ stderr
submit(Callable)ถูก​เก็บ​ไว้​ใน Futureเห็น​ต่อ​เมื่อ​เรียก get()

งาน​ที่ submit แล้ว​ไม่มี​ใคร​เรียก get() คือ​ความ​ผิด​พลาด​ที่​หาย​ไป​ตลอด​กาล · มัน​ไม่​ขึ้น​ที่ไหน​เลย ไม่มี log ไม่มี stderr และ​โปรแกรม​เดิน​ต่อ​เหมือน​ทุก​อย่าง​ปกติ

และ​เมื่อ​คุณ​เรียก get() ความ​ผิด​พลาด​จริง​ยัง​ไม่​โผล่​มา​ตรงๆ · มัน​มา​ห่อ​อยู่​ใน ExecutionException ซึ่ง​ต้อง getCause() อีก​ชั้น​ถึง​จะ​เจอ​ตัว​จริง

คน​ที่มา​จาก C# กับ JavaScript สะดุด​คนละ​ที่

จาก C# คุณ​จะ​มอง​หา async กับ await และ​จำ​มา​ว่าการ​เรียก .Result ที่​บล็อก​คือ anti-pattern · Java ไม่มี​ทั้ง​สอง​อย่าง​และ​ไม่มี​การ​ระบาย​สี​ฟังก์ชัน​เลย · f.get() ที่​บล็อก​อยู่​บน virtual thread คือ​รูป​ที่​ตั้งใจ ไม่ใช่​ของ​ที่​ต้อง​เลี่ยง

จาก JavaScript Promise.all([a, b]) คืน array ของ​ผลลัพธ์ · CompletableFuture.allOf(a, b) คืน CompletableFuture<Void> คุณ​ต้อง​กลับ​ไป​อ่าน​ที​ละ​ตัว​เอง

และ​การ​แกะ​สาเหตุ​ของ CompletableFuture มี​เงื่อนไข — exceptionally ได้ CompletionException เฉพาะ​เมื่อ​ความ​ผิด​พลาด​ลอย​ออก​มา​จาก​ฟังก์ชัน​ของ stage · ถ้า​มา​จาก completeExceptionally(...) ตรงๆ คุณ​จะ​ได้​ตัว​จริง​และ getCause() เป็น null

package orderctl;
import java.util.concurrent.CompletionException;
/** แกะสาเหตุจริงออกมา · ใช้ได้ทั้งเมื่อมีตัวห่อและเมื่อไม่มี */
public final class Unwrap {
private Unwrap() { }
public static Throwable cause(Throwable e) {
return (e instanceof CompletionException && e.getCause() != null) ? e.getCause() : e;
}
}

คอร์ส​นี้​ใช้ virtual thread เป็น​ทาง​หลัก ไม่ใช่ CompletableFuture เพราะ code ที่​บล็อก​อ่าน​ง่าย​กว่า code ที่​ต่อ stage และ​ไม่​ต้อง​จำ​กฎ​การ​แกะ​ข้าง​บน​เลย

merge อ่าน​และ​เขียน​เป็น​ก้าว​เดียว ส่วน​การ​จำกัด​จำนวน​สาย​ไม่​ได้​ทำ​ด้วย​การ​ลด​จำนวน thread

เมื่อ​หลาย virtual thread เขียน​ยอด​สะสม​ตัว​เดียวกัน get แล้ว put ไม่​พอ เพราะ​มัน​คือ​สอง​ก้าว​ที่​มี​ช่อง​ให้​แทรก​ตรง​กลาง

package orderctl;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/** ยอดสะสมที่หลาย virtual thread เขียนพร้อมกันได้ */
public final class Ledger {
private final ConcurrentHashMap<String, Long> perSku = new ConcurrentHashMap<>();
private final AtomicLong total = new AtomicLong();
/** merge อ่านและเขียนเป็นก้าวเดียว · get แล้ว put คือสองก้าวที่แทรกกลางได้ */
public void record(String sku, long satang) {
perSku.merge(sku, satang, Long::sum);
total.addAndGet(satang);
}
public long total() {
return total.get();
}
/** คืนสำเนา ไม่ใช่ตัวจริง · ผู้เรียกจึงวนมันได้โดยไม่ต้องรู้กติกาข้างใน */
public Map<String, Long> snapshot() {
return Map.copyOf(perSku);
}
}

ConcurrentHashMap กับ AtomicLong เป็น​ของ​ที่ Java มี​มา​ให้ ต่าง​จาก Go ที่​ต้อง​ห่อ map ธรรมดา​ด้วย sync.Mutex เอง — และ map ของ Go ที่​ถูก​เขียน​พร้อม​กัน​โดย​ไม่มี​ตัว​กั้น ฆ่า​โปร​เซ​ส​ทั้ง​ตัว​ด้วย fatal error ที่ recover รับ​ไม่​ได้

ส่วน​การ​จำกัด​จำนวน​สาย​ที่​ยิง​ออก​ไป​พร้อม​กัน ห้าม​ทำ​ด้วย​การ​ใช้ pool ขนาด​เล็ก · JEP 444 เขียน​เอง​ว่า​ให้​ใช้​เครื่อง​มือ​ที่​ออกแบบ​มา​เพื่อ​การ​นั้น “such as semaphores.”

package orderctl;
import java.util.concurrent.Semaphore;
import java.util.function.Supplier;
/** จำกัดจำนวนสายที่ยิงออกไปพร้อมกัน โดยไม่ลดจำนวน virtual thread */
public final class Gate {
private final Semaphore permits;
public Gate(int limit) {
this.permits = new Semaphore(limit);
}
public <T> T call(Supplier<T> task) throws InterruptedException {
permits.acquire();
try {
return task.get();
} finally {
permits.release();
}
}
}

ความ​ต่าง​อยู่​ที่​ว่า​ใคร​รอ · pool ขนาด​เล็ก​ทำให้ งาน เข้า​คิว​รอ thread ส่วน Semaphore ปล่อย​ให้​ทุก​งาน​มี thread ของ​ตัว​เอง แล้ว​ให้ thread รอ​ใบ​อนุญาต

คน​ที่​เขียน Java เมื่อ​สอง​ปี​ก่อน​ยัง​ไล่​ถอด `synchronized` อยู่

สำนวน​ที่​คุณ​จะ​ค้น​เจอ​คือ “เปลี่ยน synchronized เป็น ReentrantLock เพื่อ​แก้ pinning” พร้อม​กับ​ให้​ตั้ง -Djdk.tracePinnedThreads=full เพื่อ​หา​จุด​ที่​ตรึง

JEP 491 ที่​ลง​ใน JDK 24 จบ​งาน​นั้น​แล้ว และ system property ตัว​นั้น​ถูก​ถอด​ออก​ไป ตั้ง​แล้ว​ไม่มี​ผล​อะไร​เลย · แต่​ผู้​อ่าน​ที่​ยัง​ติด​อยู่​ที่ 21 หรือ 23 ยัง​เจอ​ปัญหา​เดิม​จริง

เหตุผล​ที่​เหลือ​ของ ReentrantLock มี​สาม​ข้อ และ​ไม่มี​ข้อ​ไหน​เกี่ยว​กับ pinning เลย

  • กำหนด​เวลา​รอ​สูงสุด​ได้
  • ถูก interrupt ได้
  • มี Condition ได้​หลาย​ตัว​ต่อ​หนึ่ง​ล็อก

เว็บ​นี้​มีหน้า​ที่​วัด​เรื่อง​นี้​ข้าม​หลาย​รุ่น​พร้อม​ช่อง​ควบคุม​อยู่​แล้ว อ่าน​ต่อ​ได้ที่ JEP 491 แก้​ปัญหา pinning แล้ว และ​เหลือ​บาน​ที่​ไม่มี​ใคร​พูด​ถึง

HttpExchange เป็น abstract class ที่​มี abstract method 17 ตัว การ​ปลอม​มัน​จึง​ไม่ใช่​ทาง​ที่​คุ้ม

javap com.sun.net.httpserver.HttpExchange บน JDK 25 แสดง public abstract method 17 ตัว · เขียน stub ให้​ครบ​ทุก​ตัว​เพื่อ​เทสต์​ตรรกะ​บรรทัด​เดียว​คือ​การ​เขียน​ของ​ทิ้ง 17 ชิ้น

ชั้น​ที่​ถูก​คือ เทสต์ Quotes.quote ตรงๆ เพราะ​มัน​เป็น​ฟังก์ชัน​ที่​รับ string คืน string และ​ไม่รู้​จัก HTTP เลย · นั่น​คือ​เหตุผล​ที่ QuoteHandler รับ​ตรรกะ​เข้า​มา​ทาง constructor

เมื่อ​จำเป็น​ต้อง​ตรวจ​ของ​ที่​อยู่​ใน​ชั้น HTTP จริง เช่น สถานะ 405 หรือ header ให้​เปิด​เซิร์ฟเวอร์​จริง​แล้ว​ผูก port 0 ให้​ระบบ​เลือก port ว่าง​ให้

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี junit-jupiter ใน pom ก่อน
@BeforeAll
static void start() throws Exception {
server = HttpServer.create(new InetSocketAddress("localhost", 0), 0); // 0 = ให้ OS เลือก
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.createContext("/quote", new QuoteHandler(Quotes::quote));
server.start();
base = URI.create("http://localhost:" + server.getAddress().getPort());
client = HttpClient.newHttpClient();
}

port ตายตัว​ทำให้​เทสต์​สอง​ชุด​รัน​พร้อม​กัน​ไม่​ได้ และ​แดง​ด้วย BindException ที่​ไม่​เกี่ยว​กับ​สิ่ง​ที่​เทสต์​ตรวจ​เลย · 0 ปิด​ช่อง​นั้น​ทั้ง​ช่อง

เซิร์ฟเวอร์​ที่ `stop()` ไป​แล้ว เอา​กลับ​มา​เปิด​ใหม่​ไม่​ได้

javadoc ของ HttpServer.stop เขียน​ไว้​ว่า “Once stopped, a HttpServer cannot be re-used.”

แปล​ว่า @AfterAll ที่​เรียก stop แล้ว​เก็บ​ตัวแปร​เดิม​ไว้​ให้​ชุด​ถัด​ไป จะ​ได้​ของ​ที่​ใช้​ไม่​ได้ · ต้อง create ใหม่​ทุก​ครั้ง

อีก​ข้อ​ที่​ควร​รู้​ตอน​อ่าน​ผล​เทสต์ — HttpClient เจรจา HTTP/2 ก่อน​แล้ว​ค่อย​ถอย​ลง HTTP/1.1 · ยิง​เข้า com.sun.net.httpserver จึง​ได้ response.version() เป็น HTTP_1_1 เสมอ

และ close(), shutdown(), awaitTermination() ของ HttpClient เป็น​ของ​ที่มา​ที่ Java 21 · tutorial ที่​เขียน​ก่อน​หน้า​นั้น​จะ​ไม่มี​รูป try (HttpClient c = ...) ให้​เห็น

สี่​แถว​ที่​บท​นี้​แตะ​จริง และ​ทุก​แถว​มี​ราคา​ทั้ง​สอง​ฝั่ง

คอร์ส Go บท​ที่ 8 ปิด​กล่อง​เปิด​บท​ด้วย​ประโยค​ว่า​ทุก​อย่าง​ยัง standard library ล้วน ไม่มี router ภายนอก​และ​ไม่มี library ของ​เทสต์​เพิ่ม · คอร์ส​นี้​พูด​ได้​แค่​ครึ่ง​แรก

เรื่องGo ให้​มา​ใน​กล่องJava ให้​มา​ใน​กล่องราคา​ที่​แต่ละ​ฝั่ง​จ่าย
routingmux.HandleFunc("POST /orders", h) อ่าน method และ​ตัวแปร​ใน path ได้ ตอบ 405 ให้​เองcreateContext("/quote", h) จับ prefix ล้วน ไม่​ดู methodJava ต้อง​เขียน​บรรทัด​ตรวจ method เอง · pattern ของ Go เป็น​ข้อความ​ล้วน​ที่​พัง​ตอน​ลง​ทะเบียน ไม่ใช่​ตอน​แปล
เทสต์​ระดับ HTTPnet/http/httptest ยิง​ผ่าน mux ได้​โดย​ไม่​เปิด portไม่มี​อะไร​เลย และ​ไม่มี test framework ด้วยJava ต้อง​เปิด port จริง​ด้วย port 0 และ​เพิ่ม junit-jupiter ใน pom.xml ทุก​โปร​เจ​กต์
การ​จับ data racego test -race ชี้​บรรทัด​ให้ไม่มี​ใน toolchain มาตรฐานของ Go จับ​เฉพาะ race ที่​เกิด​จริง​ตอน​รัน และ​ทำให้​ช้า​ลง​ชัดเจน · Java ชดเชย​ด้วย​วินัย คือ​ใช้ ConcurrentHashMap กับ AtomicLong แทน​ตัวแปร​ดิบ
ความ​ล้ม​เหลว​ของ​สาย​เดียวเอกสาร​อธิบาย​ว่า​เซิร์ฟเวอร์ recover panic ใน ServeHTTP แล้ว​เขียน stack trace ลง log · แต่ panic ใน goroutine ธรรมดา​ฆ่า​โปร​เซ​ส​ทั้ง​ตัว​ด้วย exit code 2ข้อ​ยกเว้น​ฆ่า​แค่ thread นั้น โปรแกรม​เดิน​ต่อGo ดัง​เกิน​ไป — goroutine ใบ​เดียว​ใน worker pool ล้ม​แล้ว​ทั้ง​โปรแกรม​ตาย · Java เงียบ​เกิน​ไป — submit ที่​ไม่มี​ใคร get() ทำให้​ไม่มี​ใคร​รู้​เลย

แถว​สุดท้าย​คือ​แถว​ที่​สรุป​ทั้ง​คอร์ส​ได้​ดี​ที่สุด — สอง​ภาษา​เลือก​โหมด​ความ​ล้ม​เหลว​คนละ​แบบ และ​ทั้ง​สอง​แบบ​ต้องการ​วินัย​คนละ​ชุด​จาก​คน​เขียน

จุด​ที่​ทั้ง​คู่​เหมือน​กัน​มี​ข้อ​หนึ่ง ไม่มี​ภาษา​ไหน​มี async กับ await และ​ไม่มี​การ​ระบาย​สี​ฟังก์ชัน · code ที่​บล็อก​จึง​ยัง​เป็น code ที่​ถูก​ทั้ง​สอง​ฝั่ง

แปดบท​ต่อ​กัน​เป็น​เครื่อง​มือ​ตัว​เดียว และ​ตลอด​ทาง​เพิ่ม dependency ที่​ประกาศ​เอง​สอง​ตัว

บทสิ่ง​ที่​เพิ่ม​เข้า​มา
1JDK 25 ที่​ติด​ตั้ง​แล้ว และ​โปรแกรม​แรก​ที่​รัน​ด้วย java Hello.java ล้วน
2ชนิด​ของ​โดเมน​ทั้ง​ชุด Line Order Status Discount ที่​ตรวจ​ตัว​เอง​ได้
3โครง Maven, pom.xml ที่​เขียน​เอง​ทุก​บรรทัด และ​การ​อ่าน JSON เข้า record
4ยอด​รวม ส่วนลด และ​การ​สรุป​ยอด​ตาม​หมวด​ด้วย collection กับ stream
5interface ที่​ต้อง​เอ่ย​ชื่อ และ​เส้น​แบ่ง​ระหว่าง​มัน​กับ abstract class
6checked exception, การ​รายงาน​ทุก​ข้อ​ที่​ผิด​พร้อม​กัน และ try-with-resources
7บรรทัด​คำ​สั่ง exit code สถานะ​บน disk แบบ all-or-nothing และ​เทสต์​ชุด​แรก
8POST /quote ที่​เปิด​ด้วย JDK เปล่า และ​การ​คิด​เงิน​หลาย​ใบ​พร้อม​กัน

สิ่ง​ที่​เพิ่ม​เข้า​มา​ตลอด​แปดบท​มี​สอง​อย่าง​เท่านั้น — Jackson ที่​บท​ที่ 3 เพราะ Java SE ไม่มี JSON และ JUnit Jupiter ที่​บท​ที่ 7 เพราะ JDK ไม่มี test framework

ทั้ง​สอง​ข้อ​ไม่ใช่​ความ​บกพร่อง​ของ​คอร์ส มัน​คือ​ข้อ​เท็จ​จริง​ของ​ภาษา​ที่​คอร์ส Go ไม่​ต้อง​เผชิญ​เลย และ​มัน​คือ​ราคา​ที่​บท​ที่ 1 สัญญา​ไว้​ว่า​จะ​ไม่​แกล้ง​ทำ​เป็น​ไม่​เห็น

หน้าต่าง​แรก แปล​ทั้ง​โปร​เจ​กต์​ให้​จบ แล้ว​เปิด port

$ mvn -q clean verify
$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Serve
orderctl ฟังที่ http://localhost:8080

หน้าต่าง​ที่​สอง ยิง​เข้าไป · category กับ unitSatang เป็น​ชื่อ​ที่​บท​ที่ 4 กับ​บท​ที่ 3 ตั้ง​ไว้ และ Line ปฏิเสธ​คำขอ​ที่​ขาด​สอง​ตัว​นี้​ตั้งแต่ constructor

$ curl -sS -XPOST \
-d '{"id":"A-1","lines":[{"sku":"ชาไทยเย็น","category":"drink","qty":2,"unitSatang":3500}]}' \
localhost:8080/quote
{"id":"A-1","totalSatang":7000}

สิ่ง​ที่​คอร์ส​นี้​ไม่​ใช้ และ​ไป​ต่อ​จาก​ตรง​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “สิ่ง​ที่​คอร์ส​นี้​ไม่​ใช้ และ​ไป​ต่อ​จาก​ตรง​นี้”

เรื่อง​ที่​ตัด​ออก​มี​เหตุผล​ที่​ตรวจ​ได้ และ​สาม​ก้าว​ถัด​ไป​ต่อ​จาก​สิ่ง​ที่​เพิ่ง​เรียน​จบ

คุณ​จะ​ค้น​เจอ StructuredTaskScope ใน​บทความ​เรื่อง virtual thread แทบ​ทุก​หน้า คอร์ส​นี้​ไม่​ใช้​มัน และ​เหตุผล​ไม่ใช่​เรื่อง​รสนิยม

structured concurrency ยัง​เป็น preview รอบ​ที่​เจ็ด

JEP 533 ระบุ​สถานะ​ไว้​ว่า​เป็น Seventh Preview ที่ Release 27 และ​เขียน​วิธี​ใช้​ไว้​เอง​ว่า “Compile the program with javac --release 27 --enable-preview Main.java […]”

การ​เปิด​ธง​นั้น​มี​ราคา​สาม​ข้อ​ที่​ผู้​อ่าน​ต้อง​รู้​ก่อน

  • ต้อง​ใส่​ธง​ทั้ง​ตอน​แปล และ ตอน​รัน · ลืม​ข้าง​ใด​ข้าง​หนึ่ง​แล้ว​แดง
  • class file ที่​ได้​รัน​ได้​บน JDK รุ่น​ที่​แปล​มัน​เท่านั้น ต่อ​ให้​ใส่​ธง​ตอน​รัน​บน​รุ่น​อื่น​ก็​แดง
  • API ยัง​เปลี่ยน​ทุก​รุ่น​จริง · public constructor กลาย​เป็น static factory ที่ 25 แล้ว​รอบ​ถัดๆ มายัง​ขยับ​ต่อ

เมื่อไร​ที่​มัน​เลิก​เป็น preview มัน​จะ​เป็น​ของ​ที่​ควร​ใช้ · วัน​นี้​ยัง​ไม่ใช่

สาม​ก้าว​ถัด​ไป​ที่​ต่อ​จาก​บท​นี้​ตรง​ที่สุด

  • virtual thread pinning หลัง JEP 491 — การ​วัด​จริง​ข้าม​หลาย​รุ่น พร้อม​เคส​ที่​ยัง​ตรึง​อยู่​ทุก​รุ่น ซึ่ง​เป็น​ช่อง​ควบคุม​ของ​การ​วัด​ไป​ใน​ตัว
  • java.util.concurrent ทั้ง package — CountDownLatch, BlockingQueue และ​เครื่อง​มือ​ที่​บท​นี้​ไม่​ได้​แตะ
  • คอร์ส Go จาก​ศูนย์ บท​ที่ 8 — ของ​ชิ้น​เดียวกัน​ใน​อีก​ภาษา อ่าน​คู่​กับ​ตาราง​สี่​แถว​ข้าง​บน​แล้ว​คุณ​จะ​เห็น​ราคา​ทั้ง​สอง​ฝั่ง​พร้อม​กัน

คุณ​เขียน Java เป็น​แล้ว​ใน​ความ​หมาย​ที่​ใช้ได้​จริง — ประกาศ​ชนิด​ของ​โดเมน​ที่​ผิด​ไม่​ได้ จัดการ​ความ​ผิด​พลาด​ตาม​กติกา​ของ​ภาษา และ​เปิด service ที่​รับคำ​ขอ​พร้อม​กัน​ได้

สิ่ง​ที่​คอร์ส​นี้​ไม่​ได้​ให้​คือ​ประสบการณ์​กับ framework · นั่น​เป็น​คอร์ส​ของ​ตัว​เอง และ​การ​เริ่ม​ที่​นั่น​ก่อน​จะ​ทำให้​คำถาม “ภาษา​ตัดสิน​ใจ​อย่างไร” ถูก​กลบ​ด้วย​คำถาม​อื่น


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • JEP 408: Simple Web Server (เข้า​ถึง 2026-08-27) — “The package is officially supported, and we extend it with APIs to simplify server creation and enhance request handling.”
  • jdk.httpserver module summary (Java SE 25) (เข้า​ถึง 2026-08-27) — Implementation Note “does not intend to be a full-featured, high performance HTTP server”
  • com.sun.net.httpserver package summary (Java SE 25) (เข้า​ถึง 2026-08-27) — “The API provides a partial implementation of RFC 2616 (HTTP 1.1) and RFC 2818 (HTTP over TLS).”
  • HttpServer (Java SE 25) (เข้า​ถึง 2026-08-27) — ค่า​ปริยาย​ของ executor ใช้ thread ที่ start() สร้าง · “Once stopped, a HttpServer cannot be re-used.”
  • Thread (Java SE 25) (เข้า​ถึง 2026-08-27) — virtual thread เหมาะ​กับ​งาน​ที่​บล็อก​เป็น​ส่วน​ใหญ่ และ “are not intended for long running CPU intensive operations.”
  • JEP 444: Virtual Threads (เข้า​ถึง 2026-08-27) — “Instead use constructs specifically designed for that purpose, such as semaphores.”
  • JEP 491: Synchronize Virtual Threads without Pinning (Updated 2025/11/27 · เข้า​ถึง 2026-08-27) — Status Closed/Delivered · Release 24
  • JEP 517: HTTP/3 for the HTTP Client API (เข้า​ถึง 2026-08-27) — Release 26 · Non-Goal “It is not a goal to provide a server-side implementation of the HTTP/3 protocol.”
  • JEP 533: Structured Concurrency (Seventh Preview) (Updated 2026/08/19 · เข้า​ถึง 2026-08-27) — Release 27 · “Compile the program with javac --release 27 --enable-preview Main.java […]”
  • jdk.httpserver/sun/net/httpserver/ServerImpl.java ใน lib/src.zip ของ OpenJDK 25.0.4.1 (อ่าน 2026-08-27) — class Exchange เริ่ม​ที่​บรรทัด 667 · run() ที่​บรรทัด 684 · บรรทัด 903 จับ Exception แล้ว logger.log(Level.TRACE, ...) ซึ่ง​ตรง​กับ FINER ของ java.util.logging · บรรทัด 908 จับ Throwable แล้ว throw t ต่อ
  • net/http (Go) (เข้า​ถึง 2026-08-27) — “It recovers the panic, logs a stack trace to the server error log, and either closes the network connection or sends an HTTP/2 RST_STREAM”

การ​วัด​เวลา​ทั้งหมด​ใน​บท​นี้​ทำ​บน OpenJDK 25.0.4.1 · macOS arm64 · 18 core เมื่อ 2026-08-27 · ตัวเลข​ไม่​ข้าม​เครื่อง ให้​ดู​รูป​ของ​เส้น​แทน

เช็กความเข้าใจ — บทที่ 8

ข้อ 1 / 3

ส่งงานเข้า executor ด้วย `pool.submit(...)` แล้วงานนั้นโยนข้อยกเว้นออกมา · ไม่มีใครเรียก `get()` เลย จะเกิดอะไรขึ้น