คำแนะนำเกี่ยวกับคีย์เวิร์ดที่ซิงโครไนซ์ใน Java

1. ภาพรวม

บทความสั้น ๆ นี้จะเป็นการแนะนำการใช้บล็อกที่ซิงโครไนซ์ใน Java

พูดง่ายๆก็คือในสภาพแวดล้อมแบบมัลติเธรดสภาวะการแย่งชิงจะเกิดขึ้นเมื่อเธรดตั้งแต่สองเธรดขึ้นไปพยายามอัปเดตข้อมูลที่แชร์ที่ไม่แน่นอนในเวลาเดียวกัน Java มีกลไกในการหลีกเลี่ยงสภาวะการแข่งขันโดยการซิงโครไนซ์การเข้าถึงเธรดไปยังข้อมูลที่แชร์

ชิ้นส่วนของตรรกะที่ทำเครื่องหมายด้วยการซิงโครไนซ์จะกลายเป็นบล็อกที่ซิงโครไนซ์ทำให้เธรดเดียวสามารถดำเนินการได้ในเวลาใด

2. ทำไมต้องซิงโครไนซ์?

ลองพิจารณาเงื่อนไขการแข่งขันทั่วไปที่เราคำนวณผลรวมและหลายเธรดดำเนินการวิธีการคำนวณ () :

public class BaeldungSynchronizedMethods { private int sum = 0; public void calculate() { setSum(getSum() + 1); } // standard setters and getters } 

ลองเขียนแบบทดสอบง่ายๆ:

@Test public void givenMultiThread_whenNonSyncMethod() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

เราเพียงแค่ใช้ExecutorService ที่มีพูล 3 เธรดเพื่อดำเนินการคำนวณ () 1,000 ครั้ง

หากเราดำเนินการตามลำดับนี้ผลลัพธ์ที่คาดหวังจะเป็น 1,000 แต่การดำเนินการแบบมัลติเธรดของเราล้มเหลวเกือบทุกครั้งด้วยเอาต์พุตจริงที่ไม่สอดคล้องกันเช่น:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) ...

ผลลัพธ์นี้ไม่คาดคิดแน่นอน

วิธีง่ายๆในการหลีกเลี่ยงสภาวะการแข่งขันคือการทำให้เธรดการดำเนินการปลอดภัยโดยใช้คีย์เวิร์ดที่ซิงโครไนซ์

3. คำหลักที่ซิงโครไนซ์

ตรงกันคำหลักที่สามารถนำมาใช้ในระดับที่แตกต่างกัน:

  • วิธีการอินสแตนซ์
  • วิธีการคงที่
  • บล็อกรหัส

เมื่อเราใช้บล็อกที่ซิงโครไนซ์ Java ภายในจะใช้จอภาพที่เรียกว่าการล็อกจอภาพหรือการล็อกภายในเพื่อให้การซิงโครไนซ์ จอภาพเหล่านี้ถูกผูกไว้กับอ็อบเจ็กต์ดังนั้นบล็อกที่ซิงโครไนซ์ทั้งหมดของอ็อบเจ็กต์เดียวกันจึงสามารถมีเธรดเดียวที่เรียกใช้งานได้ในเวลาเดียวกัน

3.1. วิธีการอินสแตนซ์ที่ซิงโครไนซ์

เพียงเพิ่มคีย์เวิร์ดที่ซิงโครไนซ์ในการประกาศเมธอดเพื่อให้เมธอดซิงโครไนซ์:

public synchronized void synchronisedCalculate() { setSum(getSum() + 1); }

สังเกตว่าเมื่อเราซิงโครไนซ์เมธอดกรณีทดสอบจะผ่านไปโดยมีเอาต์พุตจริงเป็น 1,000:

@Test public void givenMultiThread_whenMethodSync() { ExecutorService service = Executors.newFixedThreadPool(3); SynchronizedMethods method = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(method::synchronisedCalculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, method.getSum()); }

เมธอดอินสแตนซ์จะซิงโครไนซ์กับอินสแตนซ์ของคลาสที่เป็นเจ้าของเมธอด ซึ่งหมายความว่ามีเพียงเธรดเดียวต่ออินสแตนซ์ของคลาสเท่านั้นที่สามารถเรียกใช้วิธีนี้ได้

3.2. วิธีการSynchronized Stati c

วิธีการคงที่จะซิงโครไนซ์เช่นเดียวกับวิธีอินสแตนซ์:

 public static synchronized void syncStaticCalculate() { staticSum = staticSum + 1; }

เมธอดเหล่านี้ซิงโครไนซ์กับคลาสอ็อบเจ็กต์ที่เชื่อมโยงกับคลาสและเนื่องจากมีอ็อบเจ็กต์คลาสเดียวเท่านั้นต่อ JVM ต่อคลาสเธรดเดียวเท่านั้นที่สามารถดำเนินการภายในเมธอดซิงโครไนซ์แบบคงที่ต่อคลาสโดยไม่คำนึงถึงจำนวนอินสแตนซ์ที่มี

มาทดสอบกัน:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. บล็อกที่ซิงโครไนซ์ภายในวิธีการ

บางครั้งเราไม่ต้องการซิงโครไนซ์วิธีการทั้งหมด แต่มีเพียงคำแนะนำบางส่วนเท่านั้น สิ่งนี้สามารถทำได้โดยใช้การซิงโครไนซ์กับบล็อก:

public void performSynchronisedTask() { synchronized (this) { setCount(getCount()+1); } }

ลองทดสอบการเปลี่ยนแปลง:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Notice that we passed a parameter this to the synchronized block. This is the monitor object, the code inside the block gets synchronized on the monitor object. Simply put, only one thread per monitor object can execute inside that block of code.

In case the method is static, we would pass the class name in place of the object reference. And the class would be a monitor for synchronization of the block:

public static void performStaticSyncTask(){ synchronized (SynchronisedBlocks.class) { setStaticCount(getStaticCount() + 1); } }

Let's test the block inside the static method:

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Reentrancy

The lock behind the synchronized methods and blocks is reentrant. That is, the current thread can acquire the same synchronized lock over and over again while holding it:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

ดังที่แสดงไว้ข้างต้นในขณะที่เราอยู่ในบล็อกที่ซิงโครไนซ์เราสามารถรับการล็อคจอภาพเดียวกันซ้ำ ๆ

4. สรุป

ในบทความสั้น ๆ นี้เราได้เห็นวิธีต่างๆในการใช้คีย์เวิร์ดที่ซิงโครไนซ์เพื่อให้เกิดการซิงโครไนซ์เธรด

เรายังได้สำรวจว่าสภาพการแข่งขันสามารถส่งผลกระทบต่อแอปพลิเคชันของเราได้อย่างไรและการซิงโครไนซ์ช่วยให้เราหลีกเลี่ยงสิ่งนั้นได้อย่างไร สำหรับข้อมูลเพิ่มเติมเกี่ยวกับความปลอดภัยของเธรดโดยใช้การล็อกใน Java โปรดดูบทความjava.util.concurrent.Locksของเรา

โค้ดที่สมบูรณ์สำหรับบทช่วยสอนนี้มีอยู่ใน GitHub