1. 개요

이 기사에서는 Java에서 동기화된 블록을 사용하는 방법을 배웁니다 .

간단히 말해, 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한 공유 데이터를 동시에 업데이트하려고 시도하면 경합 상태가 발생합니다. Java는 공유 데이터에 대한 스레드 액세스를 동기화하여 경합 상태를 방지하는 메커니즘을 제공합니다.

synchronized 로 표시된 논리 조각은 동기화된 블록이 되어 주어진 시간에 하나의 스레드만 실행할 수 있습니다 .

2. 왜 동기화인가?

합계를 계산하고 여러 스레드가 calculate () 메서드를 실행하는 일반적인 경쟁 조건을 고려해 보겠습니다.

public class SynchronizedMethods {

    private int sum = 0;

    public void calculate() {
        setSum(getSum() + 1);
    }

    // standard setters and getters
}

그런 다음 간단한 테스트를 작성해 보겠습니다.

@Test
public void givenMultiThread_whenNonSyncMethod() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods summation = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(summation::calculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

우리는 3개의 스레드 풀이 있는 ExecutorService를 사용하여 compute()를 1000번 실행합니다.

이를 직렬로 실행하면 예상 출력은 1000이 되지만 다중 스레드 실행은 거의 매번 일치하지 않는 실제 출력으로 실패합니다.

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

물론 이 결과가 예상하지 못한 것은 아닙니다.

경쟁 조건을 피하는 간단한 방법은 synchronized 키워드를 사용하여 작업을 스레드로부터 안전하게 만드는 것입니다.

3. 동기화된 키워드

여러 수준에서 동기화된 키워드를 사용할 수 있습니다 .

  • 인스턴스 방법
  • 정적 메서드
  • 코드 블록

동기화된 블록을 사용할 때 Java는 내부적으로 모니터 잠금 또는 고유 잠금이라고도 하는 모니터를 사용하여 동기화를 제공합니다. 이러한 모니터는 개체에 바인딩됩니다. 따라서 동일한 개체의 모든 동기화된 블록에는 동시에 실행하는 스레드가 하나만 있을 수 있습니다.

3.1. 동기화된 인스턴스 방법

메소드 선언에 synchronized 키워드를 추가하여 메소드를 동기화 할 수 있습니다.

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

메서드를 동기화하면 실제 출력이 1000으로 테스트 사례가 통과됩니다.

@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. 동기화정적 방법

정적 메서드는 인스턴스 메서드와 마찬가지로 동기화 됩니다.

 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(SynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, SynchronizedMethods.staticSum);
}

3.3. 방법 내에서 동기화된 블록

때때로 우리는 전체 방법을 동기화하지 않고 그 안에 있는 일부 지침만 동기화하기를 원합니다. 동기화를 블록에 적용하여 이를 달성할 수 있습니다 .

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

그런 다음 변경 사항을 테스트할 수 있습니다.

@Test
public void givenMultiThread_whenBlockSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedBlocks synchronizedBlocks = new SynchronizedBlocks();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(synchronizedBlocks::performSynchronisedTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, synchronizedBlocks.getCount());
}

동기화된 블록 에 this 매개변수를 전달했음을 주목하십시오 . 이것은 모니터 개체입니다. 블록 내부의 코드는 모니터 개체에서 동기화됩니다. 간단히 말해 모니터 개체당 하나의 스레드만 해당 코드 블록 내에서 실행할 수 있습니다.

메서드가 static 인 경우 개체 참조 대신 클래스 이름을 전달하고 클래스는 블록 동기화를 위한 모니터가 됩니다.

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

정적 메서드 내에서 블록을 테스트해 보겠습니다 .

@Test
public void givenMultiThread_whenStaticSyncBlock() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(SynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, SynchronizedBlocks.getStaticCount());
}

3.4. 재진입

동기화된 메서드 및 블록 뒤의 잠금은  재진입입니다. 이는 현재 스레드가 동일한 동기화된 잠금을 유지하는 동안 반복해서 획득할 수 있음을 의미합니다.

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에서 사용할 수 있습니다 .

res – REST with Spring (eBook) (everywhere)