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