ว่าด้วยเรื่อง “Synchronization” ใน Java
ใน Concurrency applicaiton นั้นจะเป็นสิ่งแวดล้อมที่มี thread ทำงานกันหลาย thread แต่ว่าถ้า thread เหล่านั้นต้องการเข้าถึงทรัพยากร (resource) เดียวกัน มันก็จะมีโอกาศที่ thread มากกว่าหนึ่งจะเข้าถึงทรัพยากรในเวลาที่พร้อมกัน และก็จะทำให้การทำงานของ multi-thread มีปัญหาได้ ซึ่งปัญหานี้ Java ได้มีกลไลรองรับไว้แล้วนั้นก็คือการให้ thread มีการประสานข้อมูลกัน (Synchronizing) ในการเข้าถึงทรัพยากรเดียวกันนี้
การประสานข้อมูลกันนี้ทำให้ thread ใด thread หนึ่งทำงานเพียง thread เดียว ณ ช่วงเวลาใดเวลาหนึ่ง และบล็อก thread อื่นไม่ให้ทำงานจนกว่า thread นั้นจะทำงานเสร็จเรียบร้อย
ทำไมต้อง Synchronization
ลองพิจารณา race condition คลาสที่มี method increase()
ที่เพิ่มค่าของ c ซึ่งเป็นทรัพยากรร่วมกันของ thread โดยจะรันหลายๆ thread ที่เรียก method increase()
class Counter {
private static int c = 0;
public static void increase() {
c++;
}
public static int value() {
return c;
}
}
ในคลาส Demo จะรัน 2 thread พร้อมกัน เพื่อเรียก method increase
10000 ครั้ง
public class Demo {
public static void main(String[] args)
throws InterruptedException { ExecutorService executors = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000).forEach(i ->
executors.submit(Counter::increase));
executors.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(Counter.value());
}
}
Output
9981
ซึ่งจะเห็นได้ว่า output จะเป็นตัวเลข random ที่ไม่ได้ตัวเลข 10000 เสมอเนื่องจากมีการ failed เนื่องจากเรียก method increase
เพื่อเข้าถึง c
พร้อมกัน นี่คือปัญหาของการเข้าถึงทรัพยากรเดียวกันของ thread มากกว่าหนึ่ง thread ที่ทำงานพร้อมกัน
ดังนั้น Synchronization จึ่งจะมาแก้ปัญหานี้
Synchronized keyword
Synchronized keyword สามารถใช้ได้ในระดับที่แตกต่างกัน ดังนี้
- Instance method
- Static method
- Code block
เมื่อใช้ Synchronized code block นั้น Java จะใช้วิธีการ monitor คือ monitor lock หรือ intensic lock ในการทำ Sychronization
Synchronized — Instance method
การใช้ synchronized keyword กับ instance method ทำได้ด้วยการเพิ่ม synchronized
เข้าไปในการประกาศ instance method เพื่อทำให้ method เป็น Synchronization
class Counter {
private int c = 0;
public synchronized void increase() {
c++;
}
public int value() {
return c;
}
}
Synchronized method จะ synchronization บน instance ของ class ที่เป็นเจ้าของ method นั้นหมายความว่า 1 thread ต่อ instance ของ class ที่สามารถ invoke ให้ method ทำงาน จะทำให้ output ออกมาเป็น 10000 เสมอ
public class Demo {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(2);
Counter counter = new Counter();
IntStream.range(0, 10000).forEach(i ->
executors.submit(counter::increase));
executors.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(counter.value());
}
}
Output
10000
Synchronized — Static method
การใช้ synchronized keyword กับ static method ทำได้ด้วยการเพิ่ม synchronized
เข้าไปในการประกาศ static method เพื่อทำให้ method เป็น Synchronization คล้ายกับ instance method
class Counter {
private static int c = 0;
public static synchronized void increase() {
c++;
}
public static int value() {
return c;
}
}
Synchronized method จะ synchronization บน Class object ที่เชื่อมกันด้วยคลาสและเพียง 1 Class object เท่านั้นที่อยู่ใน JVM เพียงหนึ่ง thread ทำงานและ invoke ให้ static method ทำงานได้ในช่วงเวลาใดเวลาหนึ่งเท่านั้นและจะบล็อก thread อื่นไม่ให้ invoke static method นี้ จะทำให้ output ออกมาเป็น 10000 เสมอ
public class Demo {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000).forEach(i ->
executors.submit(Counter::increase));
executors.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(Counter.value());
}
}
Output
10000
Synchronized — Code block
บางทีเราไม่ต้องการทำ synchronization ทั้ง method แต่ว่าต้องการเพียงแค่บางส่วนของโค้ดเท่านั้น ซึ่งสามารถใช้ synchronized keyword ครอบส่วนของโค้ดที่เราต้องการทำให้มันทำงานสอดคล้องกัน
class Counter {
private int c = 0;
public void increase() {
synchronized (this) {
c++;
}
}
public int value() {
return c;
}
}
จะเห็นได้ว่าเราใส่ this
ใน synchronized block ซึ่งเป็นการบ่งบอกว่าให้ instance นี้เป็น monitor object โค้ดที่อยู่ใน block เป็น synchronization ที่ทำงานสอดคล้องกัน ซึ่งมีเพียง 1 thread ต่อ monitor object ที่สามารถ execute ส่วนที่อยู่ใน block ได้ จะทำให้ output ออกมาเป็น 10000 เสมอ
public class Demo {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(2);
Counter counter = new Counter();
IntStream.range(0, 10000).forEach(i ->
executors.submit(counter::increase));
executors.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(counter.value());
}
}
Output
10000
แต่ถ้ากรณี static method
ที่ไม่สามารถใช้ this
keyword ได้ เราจะส่ง class name เข้าไปใน syncronized block ซึ่งจะทำให้คลาสเป็น monitor object เพื่อ syncronization monitoring
class Counter {
private static int c = 0;
public static void increase() {
synchronized (Counter.class) {
c++;
}
}
public static int value() {
return c;
}
}public static void main(String[] args)
throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000).forEach(i ->
executors.submit(Counter::increase));
executors.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(Counter.value());
}
}