Synchronized เป็นกลไกการซิงโครไนซ์ในตัวของภาษา Java ที่ให้การเข้าถึงส่วนสำคัญของโค้ดแบบเอกสิทธิ์เฉพาะบุคคล ตามข้อมูลของ Oracle, 2024 ตัวปรับแต่ง synchronized รับประกันว่าเพียงเธรดเดียวเท่านั้นที่สามารถดำเนินการเมธอดหรือบล็อกที่ทำเครื่องหมายไว้ใน ช่วงเวลาใดเวลาหนึ่ง กลไกนี้มีพื้นฐานมาจากมอนิเตอร์ (monitor) ซึ่งเป็นแนวคิดพื้นฐานของระบบปฏิบัติการที่ช่วยให้การทำงานที่ถูกต้องของแอปพลิเคชันแบบหลายเธรดทุกระดับความซับซ้อน
ประเด็นสำคัญ
Synchronized เป็นคำสำคัญใน Java ที่รับประกันว่าเพียงเธรดเดียวเท่านั้นที่ดำเนินการส่วนของโค้ดที่ได้รับการป้องกันในเวลาใดเวลาหนึ่ง ป้องกันการเสียหายของข้อมูลระหว่างการเข้าถึงพร้อมกัน ปรากฏใน Java เวอร์ชันแรกและยังคงเป็นวิธีที่ง่ายที่สุดในการรับประกันความปลอดภัยของเธรดสำหรับนักพัฒนาทุกระดับ
ตัวปรับแต่ง synchronized แก้ปัญหาสองอย่าง: การแยกส่วนร่วมกัน (mutual exclusion) และ การมองเห็นการเปลี่ยนแปลง (visibility) เมื่อเธรดออกจากบล็อก synchronized การเปลี่ยนแปลงทั้งหมดจะมองเห็นได้กับเธรดอื่นที่เข้าสู่บล็อกที่ซิงโครไนซ์บนออบเจกต์เดียวกัน
Synchronized สามารถนำไปใช้กับเมธอดทั้งหมดหรือกับ บล็อกโค้ด ใดก็ได้โดยระบุออบเจกต์มอนิเตอร์ ในทั้งสองกรณี JVM จะแทรกคำสั่ง monitorenter และ monitorexit ในระดับไบต์โค้ด
ในแอปพลิเคชันแบบหลายเธรดที่ไม่มีการซิงโครไนซ์ จะเกิด สภาวะการแข่งขัน (race condition) เมื่อสองเธรดแก้ไขข้อมูลเดียวกันพร้อมกัน ทำให้เกิดผลลัพธ์ที่คาดเดาไม่ได้ Synchronized กลายเป็นเครื่องมือแรกและหลักของ Java ในการแก้ปัญหานี้ โดยให้รูปแบบคำสั่งแบบประกาศที่เรียบง่ายซึ่งนักพัฒนาทุกคนสามารถเข้าถึงได้
กลไก synchronized มีพื้นฐานมาจากแนวคิดของมอนิเตอร์ (monitor) ซึ่งเป็นสิ่งดั้งเดิมสำหรับการซิงโครไนซ์ระดับสูงที่สร้างอยู่ในทุกออบเจกต์ Java มอนิเตอร์จะเชื่อมโยงกับออบเจกต์เมื่อมีการใช้บล็อก synchronized ครั้งแรกบนออบเจกต์นั้น
ทุกออบเจกต์ใน Java มี มอนิเตอร์ ที่เกี่ยวข้อง เมื่อเธรดเข้าสู่บล็อก synchronized เธรดจะได้รับการครอบครองมอนิเตอร์ของออบเจกต์ หากมอนิเตอร์ถูกครอบครองโดยเธรดอื่นอยู่แล้ว เธรดจะถูกบล็อกจนกว่าจะปล่อย ในไบต์โค้ด สิ่งนี้สอดคล้องกับคำสั่งคู่ monitorenter และ monitorexit
JVM ปรับปรุงประสิทธิภาพของ synchronized ผ่านหลายระดับ: biased locking (การล็อกแบบเอนเอียง) สำหรับการเข้าถึงเธรดเดียว lightweight locking (การล็อกแบบเบา) สำหรับการแข่งขันต่ำ และ heavyweight locking (การล็อกแบบหนัก) สำหรับการแข่งขันสูงที่เกี่ยวข้องกับระบบปฏิบัติการ ระดับเหล่านี้ช่วยเพิ่มประสิทธิภาพโดยไม่ต้องเปลี่ยนโค้ด
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized สร้างความสัมพันธ์แบบ happens-before: การกระทำทั้งหมดในเธรดก่อนออกจากบล็อก synchronized จะมองเห็นได้กับเธรดอื่นหลังจากเข้าสู่บล็อกที่ซิงโครไนซ์บนออบเจกต์เดียวกัน สิ่งนี้รับประกันไม่เพียงแต่การแยกส่วนร่วมกัน แต่ยังรวมถึงความสอดคล้องของข้อมูลสำหรับทุกเธรด
Java มีสองวิธีในการใช้ synchronized: ในระดับเมธอดและในระดับบล็อก การเลือก ระหว่างทั้งสองส่งผลต่อประสิทธิภาพและความละเอียดของการซิงโครไนซ์
การทำเครื่องหมายเมธอดด้วยตัวปรับแต่ง synchronized จะซิงโครไนซ์มันบน อินสแตนซ์ปัจจุบัน โดยอัตโนมัติ (สำหรับเมธอดอินสแตนซ์) หรือบนออบเจกต์ Class (สำหรับเมธอดแบบสแตติก) นี่เป็นวิธีที่ง่ายที่สุดในการรับประกันการแยกส่วนร่วมกัน แต่มักจะมากเกินไปหากส่วนสำคัญเป็นเพียงส่วนเล็กของเมธอดและโค้ดที่เหลือไม่ต้องการการซิงโครไนซ์
บล็อก synchronized ให้การควบคุมที่แม่นยำ: คุณระบุ ออบเจกต์มอนิเตอร์ และซิงโครไนซ์เฉพาะส่วนของโค้ดที่จำเป็น โดยปล่อยให้เมธอดที่เหลืออยู่นอกการล็อก ซึ่งช่วยลดระยะเวลาการถือครองมอนิเตอร์และปรับปรุงประสิทธิภาพโดยรวมของแอปพลิเคชันในสภาพแวดล้อมแบบหลายเธรด เนื่องจากเธรดอื่นสามารถดำเนินการโค้ดที่ไม่เกี่ยวข้องแบบขนานได้โดยไม่ต้องรอการปล่อยมอนิเตอร์
class DataProcessor {
private final Object lock = new Object();
public void process() {
// โค้ดนอกส่วนสำคัญ - ไม่มีการซิงโครไนซ์
prepareData()
synchronized (lock) {
// เฉพาะบล็อกนี้ที่ได้รับการป้องกัน
updateSharedState()
}
// ดำเนินการต่อโดยไม่ล็อก
cleanup()
}
}
| เกณฑ์ | เมธอด synchronized | บล็อก synchronized |
|---|---|---|
| มอนิเตอร์ | this (อินสแตนซ์) หรือ Class | ออบเจกต์ใดก็ได้ |
| ความละเอียด | ทั้งเมธอด | เฉพาะโค้ดที่จำเป็น |
| ความสามารถในการอ่าน | สูง | ปานกลาง |
| ประสิทธิภาพ | ต่ำกว่าสำหรับเมธอดขนาดใหญ่ | สูงกว่าสำหรับส่วนสำคัญขนาดเล็ก |
ในการพัฒนา Android synchronized ถูกใช้อย่างแพร่หลายเพื่อปกป้อง SharedPreferences การเข้าถึงฐานข้อมูล และส่วนประกอบ UI อย่างไรก็ตาม การใช้งานบนเธรดหลักไม่แนะนำอย่างยิ่งเนื่องจากความเสี่ยงที่อินเทอร์เฟซจะค้าง
SharedPreferences ใน Android ให้ ความปลอดภัยพื้นฐานของเธรด แต่เมื่อแก้ไขจากหลายเธรดผ่าน Editor อาจจำเป็นต้องมีการซิงโครไนซ์ภายนอก บล็อก synchronized ที่มีออบเจกต์ล็อกแยกต่างหากรับประกันความสอดคล้องของการเปลี่ยนแปลง
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
ข้อจำกัดหลักของ synchronized บน Android คือ การบล็อกเธรด ซึ่งแตกต่างจาก coroutine ที่มี Mutex synchronized จะบล็อกเธรดระบบทั้งหมด บนเธรดหลัก สิ่งนี้ทำให้เกิด ANR ในการพัฒนา Android สมัยใหม่ แนะนำให้แทนที่ synchronized ด้วย coroutine (suspend Mutex) หรือประเภทอะตอมมิก (AtomicInteger)
Java และ Kotlin สมัยใหม่มีทางเลือกหลายอย่างสำหรับ synchronized ซึ่งแต่ละทางแก้ปัญหาเดียวกันด้วยข้อจำกัดน้อยกว่าหรือประสิทธิภาพที่ดีกว่า
อินเทอร์เฟซ Lock ที่มีการใช้งาน ReentrantLock และ ReadWriteLock ให้การหมดเวลา การรอที่สามารถขัดจังหวะได้ และคิว Condition หลายรายการ มันยืดหยุ่นกว่า synchronized แต่ต้องปล่อยอย่างชัดเจนใน finally ซึ่งเพิ่มความเสี่ยงของข้อผิดพลาดเมื่อลืม unlock
AtomicInteger, AtomicLong, AtomicReference และคลาสอื่นใช้ขั้นตอนวิธีแบบ Lock-Free ที่อิงตาม CAS (Compare-And-Swap) พวกมันเร็วกว่า synchronized อย่างมากในสถานการณ์ที่มีการแข่งขันปานกลางเนื่องจากไม่บล็อกเธรด แต่ทำการลองใหม่แบบมองโลกในแง่ดีโดยไม่ต้องสลับบริบทของเคอร์เนลระบบปฏิบัติการ
ThreadLocal ให้แนวทางอื่น: ตัวแปร ThreadLocal แต่ละตัวถูกแยกภายใน เธรดเดียว และไม่ต้องการการซิงโครไนซ์สำหรับการอ่านและเขียน ซึ่งช่วยขจัดความจำเป็นในการใช้ synchronized สำหรับข้อมูลที่ไม่ควรแชร์ระหว่างเธรดโดยสมบูรณ์ ThreadLocal ถูกใช้อย่างแข็งขันในเฟรมเวิร์ก (Spring, Hibernate) สำหรับจัดเก็บบริบทของธุรกรรมและเซสชัน
ในโปรเจกต์ Kotlin สำหรับ Android ทางเลือกของ synchronized คือ Mutex จาก kotlinx.coroutines มันไม่บล็อกเธรดของระบบปฏิบัติการ แต่ระงับ coroutine จนกว่าล็อกจะถูกปล่อย ซึ่งช่วยให้ใช้เธรดในพูลได้อย่างมีประสิทธิภาพและหลีกเลี่ยง ANR ระหว่างรอการปล่อยทรัพยากรเป็นเวลานาน
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
ประสิทธิภาพของ synchronized เปลี่ยนแปลงอย่างมีนัยสำคัญในเวอร์ชัน Java ล่าสุด ก่อนหน้านี้ถือว่าเป็นกลไก “หนัก” แต่ JVM สมัยใหม่ได้ขจัดค่าใช้จ่ายส่วนใหญ่ด้วยการเพิ่มประสิทธิภาพคอมไพเลอร์ JIT ขั้นสูง มาดูรายละเอียดว่าเครื่องเสมือนเร่งความเร็วโค้ดที่ซิงโครไนซ์ในรันไทม์ได้อย่างไร
คอมไพเลอร์ JIT ของ JVM ใช้การเพิ่มประสิทธิภาพหลายอย่าง: biased locking จะกำจัดการซิงโครไนซ์หากล็อกถูกครอบครองโดยเธรดเดียวกันเสมอ; lock coarsening จะรวมบล็อก synchronized ที่อยู่ติดกันเป็นหนึ่งเดียว; lock elimination จะลบการซิงโครไนซ์หากออบเจกต์สามารถเข้าถึงได้โดยเธรดเดียวเท่านั้น การเพิ่มประสิทธิภาพเหล่านี้ทำให้ synchronized แทบไม่มีค่าใช้จ่ายเมื่อมีการแข่งขันต่ำ
JVM กำหนดระดับการแข่งขันสำหรับแต่ละออบเจกต์: เมื่อไม่มี การแข่งขัน biased locking จะถูกเปิดใช้งาน; เมื่อเธรดที่สองปรากฏขึ้น ล็อกจะเปลี่ยนเป็นโหมดเบาพร้อมการรอแบบหมุน (spin); และเฉพาะในระหว่างการรอนานเท่านั้นที่จะขยายไปเป็นโหมดหนักด้วยมูเท็กซ์ของระบบ การขยายนี้เกิดขึ้นโดยอัตโนมัติและนักพัฒนาไม่จำเป็นต้องเลือกกลยุทธ์ด้วยตนเอง
ในการวัดประสิทธิภาพสมัยใหม่ (Java 17+) synchronized แสดงประสิทธิภาพที่เทียบเท่ากับ ReentrantLock ในการแข่งขันต่ำและปานกลาง ในการแข่งขันสูง Lock อาจได้เปรียบด้วยคิวรอที่มีประสิทธิภาพมากกว่าพร้อมการรองรับหมดเวลาและการขัดจังหวะ สำหรับระบบที่มีโหลดสูงซึ่งการแข่งขันคงที่ ReentrantLock ในโหมด fair ให้พฤติกรรมที่คาดเดาได้มากกว่า
คลาสอะตอมมิก (AtomicInteger, AtomicReference) ยังคงเร็วที่สุดสำหรับตัวนับและแฟลกง่ายๆ ด้วย การใช้งาน Lock-Free ที่อิงตาม CAS พวกมันไม่บล็อกเธรดเลย — เมื่อเกิดความขัดแย้ง การดำเนินการก็แค่ลองใหม่ในลูป สิ่งนี้ให้ประสิทธิภาพที่เพิ่มขึ้น 3-5 เท่าเมื่อเทียบกับ synchronized ในการดำเนินการเพิ่มตัวนับด้วย 4-8 เธรด
คำถามที่พบบ่อย
Synchronized ให้ทั้งการแยกส่วนร่วมกันและการมองเห็น Volatile รับประกันเฉพาะการมองเห็นการเปลี่ยนแปลง — การเขียนไปยังตัวแปร volatile จะมองเห็นได้กับทุกเธรด แต่ไม่ได้ป้องกันการแก้ไขพร้อมกัน กล่าวคือ ไม่ป้องกันสภาวะการแข่งขัน
ได้ deadlock เป็นไปได้กับการซิงโครไนซ์แบบซ้อนโดยใช้ลำดับมอนิเตอร์ต่างกัน ตัวอย่างเช่น เธรดหนึ่งเรียก synchronized(a) { synchronized(b) } ขณะที่อีกเธรดเรียก synchronized(b) { synchronized(a) } หลีกเลี่ยงบล็อก synchronized แบบซ้อนหรือกำหนดลำดับมอนิเตอร์ที่สอดคล้องกัน
มอนิเตอร์ เป็นกลไกการซิงโครไนซ์ที่เกี่ยวข้องกับทุกออบเจกต์ Java มันรับประกันว่าเพียงเธรดเดียวเท่านั้นที่ดำเนินการโค้ด synchronized บนออบเจกต์นั้น มอนิเตอร์ประกอบด้วยล็อก คิวรอ และกลุ่มของเธรดที่รอการแจ้งเตือนผ่าน wait/notify
ในเวอร์ชัน Java สมัยใหม่ (17+) synchronized ไม่ด้อยกว่า Lock ในด้านประสิทธิภาพด้วยการเพิ่มประสิทธิภาพ JIT (biased locking, lock coarsening) Lock ถูกเลือกไม่ใช่เพราะความเร็ว แต่เพราะความสามารถเพิ่มเติม: การหมดเวลา การรอที่สามารถขัดจังหวะได้ และคิว Condition หลายรายการ
เมธอดสแตติก synchronized ใช้มอนิเตอร์ของออบเจกต์ Class ของคลาสที่กำหนด ไม่ใช่อินสแตนซ์ ซึ่งหมายความว่าการซิงโครไนซ์มีผลกับทุกอินสแตนซ์ของคลาส เมธอดที่ไม่ใช่สแตติกและสแตติก synchronized ใช้มอนิเตอร์ต่างกันและไม่บล็อกซึ่งกันและกัน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม