자바의 synchronized 키워드 동기화 범위 정리하기!
이전에 게시했던 ReentrantLock과 유사하게 자바에선 synchronized 키워드를 통해 특정 클래스 및 인스턴스에 대한 동기화를 제공한다.
개인적으론 synchronized 키워드를 직접 코드단에 사용하기보단 코어한 라이브러리들을 뜯어보며 발견한 경우가 대부분이었기에 동기화라는 키워드로 느낌만 대충 이해하고 있었다.
하지만 synchronized는 사용 방식에 따라 락 범위에 약간의 차이가 있고, 이를 이해하지 않고 사용하면 예상치 못한 문제를 만나게 될 수 있다.
synchronized를 락의 관점에서 정리해보자!
1. synchronized method
아래와 같이 메서드 단위로 synchronized를 선언할 수 있다.
public class SampleClass {
public synchronized void synchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing.");
Thread.sleep(2);
System.out.println(callNumber + " finished print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
메서드 단위의 synchronized는 인스턴스에 락을 건다.
따라서, 동일 인스턴스에서 해당 메서드를 동시에 호출하면 동기화가 보장된다.
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
SampleClass instance = new SampleClass();
Runnable runnable1 = () -> instance.synchronizedPrint("1");
Runnable runnable2 = () -> instance.synchronizedPrint("2");
executorService.submit(runnable1);
executorService.submit(runnable2);
}
}
물론 인스턴스에 대하여 락을 건다는 것은 synchronized가 선언된 메서드부에 한해서만 적용된다.
다시 말해, 아래와 같이 동일 인스턴스에서 synchronized가 선언된 메서드와 그렇지 않은 메서드를 동시에 실행하더라도 synchronized가 선언되지 않은 메서드까지 락이 걸리진 않는다.
public class SampleClass {
public synchronized void synchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing.");
Thread.sleep(2);
System.out.println(callNumber + " finished print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void nonSynchronizedPrint(String callNumber) {
System.out.println(callNumber + " is non synchronized printing.");
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
SampleClass instance = new SampleClass();
Runnable runnable1 = () -> instance.synchronizedPrint("1");
Runnable runnable2 = () -> instance.nonSynchronizedPrint("2");
executorService.submit(runnable1);
executorService.submit(runnable2);
}
}
그렇다면 만약 서로 다른 두 메서드에 대해 synchronized를 선언하고 이를 동시에 실행시키면 동기화가 일어날까?
public class SampleClass {
public synchronized void synchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing.");
Thread.sleep(2);
System.out.println(callNumber + " finished print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void anotherSynchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing in another synchronized print.");
Thread.sleep(2);
System.out.println(callNumber + " finished print in another synchronized print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
SampleClass instance = new SampleClass();
Runnable runnable1 = () -> instance.synchronizedPrint("1");
Runnable runnable2 = () -> instance.anotherSynchronizedPrint("2");
executorService.submit(runnable1);
executorService.submit(runnable2);
}
}
동기화가 발생했다!
정리하면, synchronized가 선언된 메서드는 동일 인스턴스의 synchronized가 선언된 다른 메서드들과도 락을 공유한다고 할 수 있다.
2. synchronized static method
그렇다면 static 메서드에 synchronized를 선언하면 어떻게 될까?
static 으로 선언된 메서드 및 변수들은 인스턴스에서 관리되는 값이 아니므로 아예 클래스 단위로 락이 걸릴 것이라고 예상된다.
아래의 코드를 통해 테스트해보자.
public class SampleClass {
public static synchronized void staticSynchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing in static synchronized print.");
Thread.sleep(2);
System.out.println(callNumber + " finished print in static synchronized print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Runnable runnable1 = () -> SampleClass.staticSynchronizedPrint("1");
Runnable runnable2 = () -> SampleClass.staticSynchronizedPrint("2");
executorService.submit(runnable1);
executorService.submit(runnable2);
}
}
예상했듯 static으로 선언된 메서드는 클래스 단위로 동기화된다!
그렇다면 서로 다른 두 인스턴스가 synchronized가 선언되지 않은 메서드에서 static synchronized 메서드를 호출하면 어떻게 될까?
public class SampleClass {
public static synchronized void staticSynchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " is printing in static synchronized print.");
Thread.sleep(2);
System.out.println(callNumber + " finished print in static synchronized print.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void runStaticSynchronizedPrint(String callNumber) {
try {
System.out.println(callNumber + " trying to start static synchronized print");
Thread.sleep(2);
staticSynchronizedPrint(callNumber);
System.out.println(callNumber + " trying to finish static synchronized print");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
SampleClass instance1 = new SampleClass();
SampleClass instance2 = new SampleClass();
Runnable runnable1 = () -> instance1.runStaticSynchronizedPrint("1");
Runnable runnable2 = () -> instance2.runStaticSynchronizedPrint("2");
executorService.submit(runnable1);
executorService.submit(runnable2);
}
}
테스트 결과 각 인스턴스는 synchronized가 선언되지 않은 메서드를 동기화없이 호출하지만, static synchronized가 선언된 메서드 접근 시 동기화되는 것을 확인할 수 있다!
정리하기
위 내용을 최종적으로 정리하면 다음과 같다!
1. static으로 선언되지 않은 synchronized는 synchronized가 선언된 인스턴스의 모든 메서드와 락을 공유한다. 즉, 서로 다른 synchronized 메서드이더라도 동기화된다.
2. synchronized로 선언된 메서드가 인스턴스 자체에 락을 거는 것은 아니다. 따라서 synchronized가 선언되지 않은 메서드들까지 동기화가 일어나진 않는다.
3. static으로 선언된 synchronized 메서드는 클래스 단위로 락을 건다.
레퍼런스