Lock คือกลไกการซิงโครไนซ์ที่ให้การเข้าถึงส่วนสำคัญของโค้ดแบบเอกสิทธิ์ในแอปพลิเคชันแบบหลายเธรด ตามข้อมูลของ Oracle, 2024 อินเทอร์เฟซ Lock ให้การควบคุมการซิงโครไนซ์ที่ยืดหยุ่นมากกว่าเมื่อเทียบกับบล็อก synchronized แบบดั้งเดิม รวมถึงความพยายามในการรับล็อกด้วย การหมดเวลา และรองรับคิวรอหลายรายการ
ประเด็นสำคัญ
Lock คืออินเทอร์เฟซจากแพ็คเกจ java.util.concurrent.locks ที่ให้การดำเนินการล็อกและปลดล็อกอย่างชัดแจ้งเพื่อซิงโครไนซ์การเข้าถึงข้อมูล แตกต่างจาก synchronized ตรงที่ Lock ให้ผู้พัฒนาควบคุมกลไกการล็อกได้อย่างสมบูรณ์
อินเทอร์เฟซ Lock ถูกนำมาใช้ใน Java 5 เพื่อเป็นทางเลือกแทนกลไก synchronized ในตัว เมธอดหลักคือ lock, unlock, tryLock และ lockInterruptibly ล็อกช่วยจัดระเบียบการเข้าถึงข้อมูลอย่างปลอดภัยในสภาพแวดล้อมแบบหลายเธรด ป้องกันสภาวะการแข่งขันและความเสียหายของข้อมูล
ข้อได้เปรียบหลักของ Lock เหนือ synchronized คือ ความยืดหยุ่น ผู้พัฒนาสามารถพยายามรับล็อกด้วยการหมดเวลา ตรวจสอบความพร้อมใช้งานโดยไม่ถูกบล็อก หรือจัดคิวรอหลายรายการที่มีลำดับความสำคัญต่างกัน
ก่อนที่อินเทอร์เฟซ Lock จะปรากฏใน Java 5 วิธีการซิงโครไนซ์เพียงอย่างเดียวคือ synchronized ซึ่งมีข้อจำกัดหลายประการ: ไม่มีการหมดเวลา ไม่มีการรอที่สามารถขัดจังหวะได้ และคิวเดียว Doug Lea ออกแบบแพ็คเกจ java.util.concurrent โดยรวม Lock เป็นส่วนประกอบพื้นฐาน
ล็อก จัดการการเข้าถึงผ่านแฟล็กสถานะภายในและคิวรอ เมื่อเธรดเรียก lock() กลไกจะตรวจสอบว่าล็อกว่างหรือไม่ และรับล็อกหรือวางเธรดในคิวจนกว่าจะถูกปล่อย
หัวใจของล็อกทุกตัวคือ การดำเนินการเปรียบเทียบและสลับแบบอะตอมมิก (CAS) เมื่อเรียก lock() เธรดจะพยายามตั้งค่าแฟล็กไม่ว่างแบบอะตอมมิก ถ้าแฟล็กถูกตั้งไว้แล้ว เธรดจะถูกบล็อก เมื่อ unlock() แฟล็กจะถูกล้างและเธรดที่รออยู่จะถูกปลุก
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// ส่วนสำคัญ
println("เธรด ${Thread.currentThread().name} กำลังทำงาน")
} finally {
lock.unlock()
}
}
ReentrantLock ใช้ภายในรายการเชื่อมโยงสองทิศทาง (คิวล็อก CLH) โดยแต่ละเธรดที่รอจะแสดงด้วยโหนด เมื่อล็อกถูกปล่อย โหนดหัวของคิวจะถูกปลุก โหมดยุติธรรม (fair) รับประกันลำดับ FIFO ในขณะที่โหมดไม่ยุติธรรมอนุญาตให้เธรดใหม่รับล็อกก่อนเธรดที่รอเพื่อเพิ่มปริมาณงาน
ในระบบนิเวศ Java สมัยใหม่ มีการใช้งาน ล็อก หลายแบบ แต่ละแบบถูกปรับให้เหมาะสมสำหรับสถานการณ์เฉพาะ การเลือกล็อกที่ถูกต้องส่งผลโดยตรงต่อประสิทธิภาพและความน่าเชื่อถือของแอปพลิเคชันแบบหลายเธรด
ReentrantLock คือการใช้งาน Lock พื้นฐานและใช้บ่อยที่สุด รองรับการรับล็อกซ้ำโดยเธรดเดียวกัน: ถ้าเธรดถือล็อกอยู่แล้ว การเรียก lock() อีกครั้งจะไม่บล็อกเธรดนั้น ซึ่งป้องกัน deadlock ในการเรียกซ้ำ
ReadWriteLock แยกล็อกเป็นสองโหมด: อ่าน และเขียน หลายเธรดสามารถถือล็อกอ่านพร้อมกันได้ แต่การเขียนต้องการการเข้าถึงแบบเอกสิทธิ์ ซึ่งช่วยปรับปรุงประสิทธิภาพอย่างมากเมื่ออ่านบ่อยและเขียนน้อย
StampedLock คือการใช้งานล่าสุดที่นำมาใช้ใน Java 8 รองรับสามโหมด: เขียน อ่าน และอ่านแบบมองโลกในแง่ดี การอ่านแบบมองโลกในแง่ดีไม่บล็อกเธรดอื่นและตรวจสอบความถูกต้องของข้อมูลหลังจากอ่าน ซึ่งให้ประสิทธิภาพดีขึ้น 10-20% เมื่อเทียบกับ ReadWriteLock
| ล็อก | เวอร์ชัน Java | โหมด | ประสิทธิภาพ |
|---|---|---|---|
| ReentrantLock | Java 5 | เอกสิทธิ์ | สูง |
| ReadWriteLock | Java 5 | อ่าน + เขียน | ปานกลาง |
| StampedLock | Java 8 | อ่าน + เขียน + มองโลกในแง่ดี | สูงมาก |
ReentrantLock คือการใช้งาน Lock ที่ได้รับความนิยมมากที่สุด ซึ่งมีคุณสมบัติหลายอย่างที่ไม่สามารถใช้ได้ใน synchronized การเข้าใจคุณสมบัติของมันเป็นสิ่งจำเป็นสำหรับการทำงานหลายเธรดอย่างมีประสิทธิภาพ
คอนสตรัคเตอร์ของ ReentrantLock รับพารามิเตอร์ fair เมื่อเป็น true ล็อกรับประกัน ลำดับ FIFO เมื่อเป็น false เธรดใหม่อาจรับล็อกก่อนเธรดที่รอ โหมดยุติธรรมป้องกันการอดอยาก แต่ลดปริมาณงานลง 10-20% เนื่องจากค่าใช้จ่ายในการดูแลคิว
แตกต่างจาก synchronized ReentrantLock รองรับ tryLock แบบมีกำหนดเวลา ถ้าไม่สามารถรับล็อกภายในเวลาที่กำหนด เธรดจะดำเนินการต่อแทนที่จะถูกบล็อกอย่างไม่มีกำหนด เมธอด lockInterruptibly อนุญาตให้ขัดจังหวะเธรดที่รอผ่าน Thread.interrupt()
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("รับล็อกแล้ว")
} finally {
lock.unlock()
}
} else {
println("ไม่สามารถรับล็อกได้")
}
}
ReentrantLock รองรับ ตัวแปรเงื่อนไข หลายตัวผ่านเมธอด newCondition() แต่ละ Condition มีคิวรอของตัวเอง ซึ่งช่วยให้สถานการณ์การปลุกที่ซับซ้อนเป็นไปได้ เมธอด await() และ signal() แทนที่ wait() และ notify() จากบล็อก synchronized แต่รองรับหลายคิว
ReadWriteLock และ StampedLock จัดการกับการเพิ่มประสิทธิภาพการเข้าถึงเมื่อการอ่านมีอิทธิพลเหนือการเขียน มีประสิทธิภาพมากกว่า ReentrantLock อย่างมากในสถานการณ์ที่การอ่านเกิดขึ้นบ่อยกว่าการเขียน
อินเทอร์เฟซ ReadWriteLock มีสองเมธอด: readLock() และ writeLock() ล็อก อ่าน สามารถถือโดยหลายเธรดพร้อมกัน ในขณะที่ล็อกเขียนเป็นเอกสิทธิ์ ตัวอย่างทั่วไปคือแคชที่ปลอดภัยสำหรับเธรด: หลายเธรดอ่านข้อมูลในขณะที่มีเพียงหนึ่งเธรดที่อัปเดตเป็นระยะ
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock เพิ่มโหมดที่สาม — tryOptimisticRead โหมดนี้ไม่บล็อกเธรดอื่น แต่เพียงบันทึกตราประทับ (stamp) ของสถานะ หลังจากอ่าน ผู้พัฒนาเรียก validate(stamp) เพื่อตรวจสอบว่าข้อมูลเปลี่ยนแปลงระหว่างการอ่านหรือไม่ ถ้าข้อมูลเปลี่ยนแปลง การดำเนินการต้องทำซ้ำ
ในแอปพลิเคชันมือถือ ล็อก ถูกใช้เพื่อประสานงานการเข้าถึงข้อมูลที่ใช้ร่วมกันระหว่างเธรด อย่างไรก็ตาม การใช้งานต้องใช้ความระมัดระวังเป็นพิเศษเนื่องจากทรัพยากรอุปกรณ์มีจำกัดและความจำเป็นในการรักษาการตอบสนองของอินเทอร์เฟซ
บน Android ReentrantLock มีประโยชน์เมื่อทำงานกับ Room แคช และไฟล์ สิ่งสำคัญที่ต้องจำ: ห้ามรับล็อกบนเธรดหลัก สำหรับโค้ดแบบอะซิงโครนัส coroutine และ Mutex จาก kotlinx.coroutines ดีกว่า เพราะจะระงับ coroutine แทนที่จะบล็อกเธรด
ใน iOS มีการใช้ NSLock มาตรฐานน้อยกว่า — นักพัฒนาชอบ DispatchQueue ที่มีแฟล็ก barrier หรือ os_unfair_lock Swift 5.7+ มีกลไกการซิงโครไนซ์ที่ทันสมัยผ่าน actors ซึ่งปกป้องสถานะโดยอัตโนมัติ
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
เพื่อหลีกเลี่ยง deadlock ให้ปฏิบัติตาม ลำดับการรับล็อกที่สอดคล้องกัน ทั่วทั้งโปรเจกต์ ใช้ tryLock แบบมีกำหนดเวลาแทน lock() ทุกที่ที่อาจมีการบล็อกเป็นเวลานาน พิจารณาใช้อัลกอริทึมแบบ Lock-Free (AtomicReference, ConcurrentHashMap) แทนล็อกแบบดั้งเดิม
การใช้ Lock ต้องมีวินัยและการปฏิบัติตามกฎหลายข้อที่ป้องกัน deadlock และประสิทธิภาพที่ลดลง แนวปฏิบัติเหล่านี้ได้รับการพัฒนาโดยชุมชน Java ตลอด 20 ปีของการใช้แพ็คเกจ java.util.concurrent
รูปแบบที่สำคัญที่สุดคือ ล็อกใน finally ไม่ว่าส่วนสำคัญจะเสร็จสมบูรณ์หรือเกิดข้อยกเว้น ล็อกต้องถูกปล่อย ซึ่งรับประกันว่าเธรดอื่นจะไม่ถูกบล็อกตลอดไปเนื่องจากข้อผิดพลาดเดียว ใน Kotlin รูปแบบนี้แก้ไขอย่างสง่างามผ่านส่วนขยาย withLock
ส่วนสำคัญควร สั้นที่สุดเท่าที่จะทำได้ อย่าทำ I/O คำขอเครือข่าย หรือการคำนวณที่ยาวนานภายในล็อก ถ้าคุณต้องการอ่านข้อมูลจากเซิร์ฟเวอร์ ให้รับข้อมูลก่อน จากนั้นรับล็อกเพียงเพื่ออัปเดตสถานะที่ใช้ร่วมกัน ซึ่งลดการแข่งขันและเพิ่มปริมาณงานของระบบ
เพื่อป้องกัน deadlock เมื่อทำงานกับหลายล็อก ให้กำหนด ลำดับการรับล็อกทั่วโลก ในโปรเจกต์ ถ้า lockA ถูกรับก่อน จากนั้น lockB — ลำดับย้อนกลับใดๆ ต้องถูกห้ามโดยกฎการตรวจสอบโค้ด ใช้ตัววิเคราะห์แบบคงที่เช่น SpotBugs และ IntelliJ Inspections สำหรับการตรวจสอบอัตโนมัติ
คำถามที่พบบ่อย
Lock คืออินเทอร์เฟซที่ชัดแจ้งพร้อมรองรับการหมดเวลาและการรอที่สามารถขัดจังหวะได้ synchronized รับและปล่อยมอนิเตอร์โดยอัตโนมัติ แต่ไม่อนุญาตให้ใช้ tryLock, lockInterruptibly หรือหลาย Conditions Lock ยืดหยุ่นกว่าแต่ต้องการการปล่อยด้วยตนเองใน finally
ล็อกแบบยุติธรรมรับประกัน ลำดับ FIFO: เธรดที่รอนานที่สุดจะได้รับล็อกก่อน ล็อกแบบไม่ยุติธรรมอาจให้การเข้าถึงแก่เธรดใหม่ก่อนเธรดที่รอ ซึ่งเพิ่มปริมาณงานแต่อาจทำให้เธรดที่รอเกิดการอดอยาก
ปฏิบัติตาม ลำดับที่แน่นอน สำหรับการรับล็อกทั้งหมด ใช้ tryLock แบบมีกำหนดเวลาแทน lock() ที่ไม่มีเงื่อนไข และลดจำนวนล็อกที่ถือพร้อมกัน การใช้โครงสร้างข้อมูลแบบ Lock-Free ก็ลดความเสี่ยงของ deadlock เช่นกัน
Condition คืออะนาล็อกของ wait/notify สำหรับ Lock ซึ่งช่วยให้มีคิวรออิสระหลายคิว แต่ละการเรียก newCondition() สร้างคิวแยกต่างหาก ให้การควบคุมการปลุกเธรดที่แม่นยำกว่าเมื่อเทียบกับคิวเดียวของ synchronized
สำหรับ Android ที่ใช้ coroutine ให้ใช้ Mutex จาก kotlinx.coroutines — มันระงับ coroutine แทนที่จะบล็อกเธรด สำหรับ iOS ที่ใช้ Swift 5.7+ actors ดีกว่าเพราะซิงโครไนซ์การเข้าถึงสถานะโดยอัตโนมัติ เก็บ ReentrantLock ไว้สำหรับโค้ดเก่าและสถานการณ์ระดับต่ำ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม