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

ค้นหาใน CooLWJ

选择搜索范围,输入关键词后按回车。

ภาษา

บริบทเธรด Java

ThreadLocal เป็นคลาสเครื่องมือที่ใช้กันค่อนข้างบ่อยในแพ็กเกจ java.lang ของ JDK ซึ่งให้ความสามารถด้านบริบทเธรด การใช้งานที่พบบ่อยที่สุดน่าจะเป็นการใช้ ThreadLocal เก็บข้อมูลสำคัญ เช่น บริบทผู้ใช้หลังเข้าสู่ระบบ

บริบทของเธรดยังสามารถแก้ปัญหาหลายอย่างที่ดูซับซ้อนได้อย่างเรียบง่าย เช่น:ปัญหาการผสานเฟรมเวิร์ก spring-mybatis:
เมื่อพัฒนาโดยอาศัยระบบนิเวศของ spring โดยทั่วไปเราจะมอบหมายให้ spring จัดการธุรกรรม โดยใช้แอนโนเทชัน transactional เพื่อกำหนดการเปิดใช้ธุรกรรม รวมถึงข้อมูลต่าง ๆ เช่น ระดับการแยกธุรกรรม เราไม่ได้เข้าถึงฐานข้อมูลผ่าน jdbc โดยตรง แต่เข้าถึงผ่านเฟรมเวิร์ก mybatis spring และ mybatis เป็นเฟรมเวิร์กสองตัวที่เป็นอิสระต่อกันโดยสิ้นเชิง แต่เวลาใช้งาน ดูเหมือนว่า mybatis จะดึง connection ของฐานข้อมูลที่ spring เปิดไว้มาทำงานได้ทันที เราแทบไม่ต้องทำอะไรเลย เพียงเพิ่มแพ็กเกจ jar ของ mybatis-spring-boot-stater ก็พอ。
จุดสำคัญที่ทำให้การออกแบบอันงดงามนี้เป็นจริงได้ก็คือ ThreadLocal หลักการโดยคร่าว ๆ คือแพ็กเกจ stater jar จะช่วยนำสิ่งหนึ่งเข้ามาให้เราในท้ายที่สุด นั่นคือMybatisAutoConfigurationและฉีดผ่าน SPI เข้าสู่คอนเทนเนอร์ bean ของบริการเรา ในMybatisAutoConfigurationกำหนดค่าไว้เรียบร้อยแล้วในmybatisที่ต้องการSqlSessionFactory,และฉีดสิ่งของ spring เองเข้าไปด้วย คือSpringManagedTransactionFactory(ได้ทำให้เกิดTransactionFactory,โรงงานธุรกรรมเป็นกลไกส่วนขยายที่ mybatis มีให้ ซึ่งสามารถมอบหมายการจัดการธุรกรรมให้ภายนอกได้)
ขั้นตอนการทำงานโดยรวมคือ เมธอดที่มีแอนโนเทชัน transactional จะถูกพร็อกซีด้วย AOP ในที่สุด คลาสพร็อกซีจะดึง connection จาก dataSource ใน advice ก่อนทำงาน แล้วเก็บลงใน ThreadLocal ในที่สุด (ที่จริงสิ่งที่อยู่ใน ThreadLocal คือ map ซึ่งมี key เป็น dataSource และ value เป็น connectionHolder ที่เทียบได้กับ connection) เมื่อ mybatis รัน sql ในภายหลัง จะเริ่มจากผ่านTransactionFactoryรับ connection เนื่องจากสิ่งที่ฉีดเข้ามาในตอนนี้TransactionFactoryเป็นSpringManagedTransactionFactory,จึงดึง connection ของ dataSource ที่ตรงกันออกมาจาก ThreadLocal。

แนะนำหลักการเบื้องต้นของ ThreadLocal:

tapd_50394669_1660562598_27.png

ThreadLocalMapก็คือตารางแฮชที่ใช้วิธีสำรวจเชิงเส้นแก้ปัญหาการชนกัน แต่ละเธรดจะสร้างขึ้นมาหนึ่งชุด และภายในออบเจ็กต์ ThreadLocal แต่ละตัวจะมีอยู่หนึ่งตัวthreadLocalHashCodeใช้คำนวณคีย์แฮช。
ลองเปรียบเทียบกับ HashMap,ThreadLocalMapการเลือกใช้อัลกอริทึมแก้ปัญหาการชนกันแบบสำรวจเชิงเส้นแทนการเชื่อมโยงเป็นสาย อาจมีข้อพิจารณาดังต่อไปนี้:
1、ข้อพิจารณาด้านหน่วยความจำ เนื่องจากลักษณะการใช้งานต่างกัน ปริมาณข้อมูลของ ThreadLocalMap โดยทั่วไปจึงน้อยกว่า HashMap มาก เมื่อข้อมูลมีจำนวนน้อย วิธีต่อโซ่จะสิ้นเปลืองพื้นที่สำหรับพอยน์เตอร์เพิ่มขึ้นมาก
2、ประสิทธิภาพ โดยต่อยอดจากข้อ 1 เมื่อมีข้อมูลจำนวนน้อย พื้นที่ที่ประหยัดได้สามารถนำไปสร้างตารางแฮชที่มีอัตราการบรรจุต่ำลง เพื่อลดการชนกันและเพิ่มความเร็วเฉลี่ยในการค้นหา

แผนภาพความสัมพันธ์ระหว่าง ThreadLocal、เธรด และ ThreadLocalMap

ปัญหา
A ในงานที่ใช้พูลเธรด มีปัญหาเรื่องการส่งต่อบริบท โดยเฉพาะในโมเดลการเขียนโปรแกรมเว็บที่ใช้ Tomcat เนื่องจาก Tomcat เองใช้พูลเธรดจัดการการโต้ตอบแบบคำขอ-การตอบกลับระหว่างเซิร์ฟเวอร์กับไคลเอนต์ เว็บแอปพลิเคชันแต่ละตัวจึงต้องจัดการส่งต่อบริบทของเธรดให้ถูกต้องด้วยตนเอง
B ปัญหาด้านประสิทธิภาพ หากแอปพลิเคชันมี ThreadLocal มากเกินไปจนตารางแฮชขยายใหญ่ ความเร็วในการค้นหาจะลดลง

สำหรับปัญหา A วิธีแก้ไขที่ใช้กันทั่วไปค่อนข้างเหมือนกัน ขอนำโค้ดในระบบของเรามาวางไว้โดยตรง
2.png
แนวทางนี้มีปัญหาสองข้อ:
1、หากภายหลังมี ThreadLocal ใหม่ที่ต้องส่งต่ออย่างถูกต้อง ก็ต้องแก้โค้ดโดยเพิ่ม ThreadLocal ใหม่นั้นเข้าไปในตรรกะ wrap หากตกหล่นก็จะเกิดบั๊ก
2、ในบางสถานการณ์ เธรดที่ส่งงานอาจเป็นผู้ประมวลผลงานนั้นเอง เช่น:

  • เมื่อพูลเธรดเต็มและ ของพูลเธรด RejectedExecutionHandler ใช้ CallerRunsPolicy ขณะนั้น งานที่ส่งเข้าพูลเธรดจะทำงานโดยตรงในเธรดที่ส่งงาน ซึ่งตรรกะการล้างข้อมูลในขั้นสุดท้ายจะทำให้บริบทของเธรดที่ส่งงานสูญหาย

  • ในทำนองเดียวกัน ใช้ ForkJoinPool (รวมถึงการทำงานแบบขนาน Stream และ CompletableFuture ,เบื้องหลังใช้ ForkJoinPool )ในสถานการณ์ดังกล่าว ส่วนที่ขยายออกมา ForkJoinTask จะทำงานโดยตรงในเธรดที่ส่งงานเข้ามา การล้างข้อมูลในตอนท้ายก็จะทำให้บริบทของเธรดที่ส่งงานสูญหายเช่นกัน

วิธีแก้ปัญหา:
ปัญหา1:หากคลาส ThreadLocal รองรับ API ที่ใช้ดูได้ว่าขณะนี้ในแอปพลิเคชันมีออบเจ็กต์ ThreadLocal ทั้งหมดกี่ตัว ปัญหาก็จะคลี่คลายทันที น่าเสียดายที่ไม่รองรับ หากจำเป็นต้องทำจริง ๆ สามารถใช้ reflection เพื่อเข้าถึงสิ่งที่อยู่ในออบเจ็กต์ thread ซึ่งก็คือinheritableThreadLocals(ก็คือหนึ่งThreadLocalMap)จากนั้นวนอ่านตารางแฮชเพื่อดึง entry ทั้งหมด ก็จะได้คู่คีย์และค่าของ threadlocal ทั้งหมด อีกแนวทางหนึ่งคือห่อหุ้ม ThreadLocal ของ jdk เพิ่มอีกชั้น เพื่อเสริมความสามารถที่กล่าวมาข้างต้น
ปัญหา 2:แก้ได้ด้วยการปรับปรุงตรรกะของ wrap:เมื่อเธรดเริ่มทำงาน ให้ดึงบริบทของเธรดปัจจุบันมาเก็บไว้ชั่วคราวก่อน แล้วใช้บริบทของเธรดที่ส่งงานมาแทนบริบทของเธรดนี้ ก่อนเธรดทำงานเสร็จให้เปลี่ยนจากการล้างบริบทเป็นการใช้ข้อมูลที่เก็บไว้ชั่วคราวคืนค่าบริบทเดิมของเธรด。

transmittable-thread-local
มีเครื่องมือสำเร็จรูปอยู่แล้ว หลักการทำงานก็คือเริ่มจากสองแนวทางข้างต้น และยังตั้งชื่อให้กระบวนการนี้ว่า CRR(capture/replay/restore) โหมด。
มีวิธีใช้งานสามแบบ:
1 ### การตกแต่ง Runnable และ Callable

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent"); 
Runnable task = new RunnableTask(); 
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);  
// =====================================================  
// Task中可以读取,值是"value-set-in-parent" 
String value = context.get();

2 ตกแต่งพูลเธรด

ExecutorService executorService = ... 
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);  
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent");  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// =====================================================  
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

3 ### การใช้งาน Java Agent เพื่อขยาย JDK คลาสที่นำพูลเธรดไปใช้งาน

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## 
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); 
context.set("value-set-in-parent");  
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## 
ExecutorService executorService = Executors.newFixedThreadPool(3);  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// ## 3. 框架下层逻辑 ## 
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

ข้อดีของการใช้วิธี java agent คือไม่ต้องแทรกแซงโค้ดของแอปพลิเคชัน ส่วนข้อเสียนั้น ผมคิดว่าปัญหาใหญ่ที่สุดคือ หากลืมใส่พารามิเตอร์ตอนเริ่ม jvm ก็อาจเกิดเหตุขัดข้องร้ายแรงได้。

ดูตรรกะของซอร์สโค้ดคร่าว ๆ
3.png

4.png 

สำหรับปัญหา B โดยทั่วไปในการพัฒนาแอปพลิเคชันมักไม่คำนึงถึงปัญหานี้ เพราะไม่ได้อ่อนไหวต่อประสิทธิภาพด้านเวลามากนัก แต่ในการพัฒนามิดเดิลแวร์ระดับล่างบางประเภท อาจให้ความสำคัญกับการปรับประสิทธิภาพมากกว่า เช่น netty เลือกที่จะพัฒนาขึ้นเองคือFastThreadLocalใช้แทน ThreadLocal ของ jdk พร้อมทั้งมีสิ่งที่ใช้ร่วมกันคือFastThreadLocalThread,ที่FastThreadLocalThreadและFastThreadLocalเมื่อใช้ร่วมกัน สามารถรับประกันความซับซ้อนด้านเวลาในการค้นหาเป็น O(1) ได้เสมอ หลักการของแนวทางนี้คือใช้พื้นที่แลกเวลา เพื่อไม่ให้ประสิทธิภาพการค้นหาลดลงเมื่อเกิดการชนกันของแฮชในตารางแฮช จึงสร้างอาร์เรย์ที่ยาวเพียงพอ แต่ละFastThreadLocalเมื่อสร้างจะได้รับดัชนีอาร์เรย์ที่ไม่ซ้ำกัน เปรียบเสมือนแต่ละคนมีช่องของตนเอง เวลาค้นหาก็ใช้ index ของตนดึงข้อมูลจากอาร์เรย์ได้โดยตรง。
ขอแปะซอร์สโค้ดให้ดูสักหน่อย
5.png


ความคิดเห็น

กำลังส่ง

บทความยอดนิยม

เลือกธีมที่ชอบ

ธีมที่เลือกจะใช้ทั่วทั้งเว็บไซต์