자바에서 동기화 문제를 해결하는 여러가지 방법이 있다. 이 글에서는 synchronized 키워드를 사용한 동기화 문제 해결에 대해 다룬다.
주로 다음 4가지 관점에서 차이를 비교한다.
인스턴스 synchronized 메서드정적 synchronized 메서드인스턴스 메서드의 synchronized 블럭정적 메서드의 synchronized 블럭모니터는 동기화 문제를 해결하기 위해 언어레벨에서 제공하는 추상화다. 세마포어를 개발자가 직접 제어하다 발생할 수 있는 문제를 해결해준다. JVM에서 모든 객체와 클래스는 모니터와 연결되어 있고 각 모니터는 객체와 클래스의 변수를 보호한다.
In the Java virtual machine, every object and class is logically associated with a monitor. For objects, the associated monitor protects the object's instance variables. For classes, the monitor protects the class's class variables.
모니터에 의해 보호되는 구역(임계구역, 이 글에서는 모니터 영역이라 한다)에 여러 스레드가 접근하게 되면 하나의 스레드만이 모니터의 소유권을 얻어 모니터 영역에 진입하게 된다. 이때 모니터의 소유권을 얻지 못한 스레드들은 Entry Set에서 대기하게 된다. 이후 모니터의 소유권이 release 되면 소유권을 얻기위해 경쟁하고 하나의 스레드가 소유권을 얻어 모니터 영역에 진입하게 된다.
모니터의 소유권을 가진 스레드가 모니터의 소유권을 잠시 release할 수 있다. 이때 스레드는 Wait Set 에서 대기하게 된다. 대기중인 스레드는 모니터를 소유한 다른 스레드가 깨워줘야 다시 모니터 영역에 진입할 수 있으며 깨워주지 않는다면 영원히 대기하게 된다.
자바의 모니터는 상호배제와 협력을 통해 스레드의 동기화를 지원한다. synchronized 키워드를 사용하면 모니터 영역의 상호배제를 달성할 수 있다. 이를 통해 모니터는 객체와 클래스 변수를 보호한다.
public synchronized void test() {}
synchronized(monitor) {}
Object 클래스는 wait(), notify(), notifyAll() 메서드가 정의되어 있다. 이 메서드는 모니터의 소유권을 스레드끼리 주고 받으며 협력을 달성할 수 있다.
synchronized는 위의 예시와 같이 메서드의 키워드로 사용할 수도 있고, 블럭형태로도 사용할 수 있다. 어떻게 사용하던 모니터 영역에는 모니터의 소유권이 있어야 진입이 가능한 상호배제를 달성하기 위한 점에서는 동일하다.
synchronized 메서드는 메서드를 모니터 영역으로 지정하고 스레드가 메서드의 인스턴스의 모니터 소유권을 얻어 모니터 영역에 진입한다.
다음 코드를 보자. 클래스 SharedResource 는 내부 list 를 갖고, cs() 메서드는 이 리스트에 접근하는 메서드다. cs() 메서드에 synchronized 키워드를 사용함으로써, 이 메서드는 동시에 하나의 스레드만 접근이 가능하도록 설정할 수 있다. sr 이라는 ShredResource 인스턴스를 생성하고, 두개의 스레드가 sr 인스턴스의 cs() 메서드를 실행한다.
// code - 1
public class Main {
public static void main(String[] args) throws Exception {
SharedResource sr = new SharedResource();
Thread t1 = new Thread(() -> sr.cs());
Thread t2 = new Thread(() -> sr.cs());
t1.start();
t2.start();
}
}
class SharedResource {
private List<Integer> list = new ArrayList<>();
public synchronized void cs() {
System.out.println("SharedResource.cs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.cs end " + Thread.currentThread());
}
}
실행 결과를 확인해 보면 한 스레드(#21)의 작업이 끝날때까지 다른 스레드(#22)의 접근이 차단되는것을 알 수 있다.

만약 synchronized 키워드가 없다면 다음과 같이 두개의 스레드(#21, #22)가 작업을 실행하고 동시에 수행되게 된다.

synchronized 키워드를 사용한 결과에서는 sr 의 모니터 영역에 접근한 스레드(#21)가 모니터의 소유권을 얻고(객체를 잠근다고 표현), 이후 모니터 영역에 접근한 스레드(#22)는 Entry Set 에서 대기하며 모니터 소유권을 기다린다.
만약 여러 스레드가 동일한 객체의 여러 synchronized 메서드에 접근하더라도 동일한 객체의 모니터는 하나이기에 한 객체에 대해서 하나의 synchronized 메서드 수행이 보장된다.
즉 synchronized 메서드는 객체 레벨의 동기화를 제공한다.
위에서 알아본 내용은 인스턴스 메서드일때 얘기다.
static 메서드, 정적 메서드일때는 인스턴스 레벨이 아닌 클래스 레벨에서 동기화가 동작한다. 모니터 설명에서 다음과 같이 설명했다.
인스턴스와 클래스는 자신과 연결된 모니터가 있고, 이 모니터는 인스턴스 변수와, 클래스 변수에 대한 보호를 한다
synchronized가 static method 에서 사용될때 클래스 레벨에서 동기화를 제공하는것 뿐 큰 차이는 없다.
아래 예시는 code - 1 를 약간 수정해 static method로 변경한 예시다.
// code - 2
public class Main {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> SharedResource.staticCs());
Thread t2 = new Thread(() -> SharedResource.staticCs2());
t1.start();
t2.start();
}
}
class SharedResource {
static private List<Integer> list = new ArrayList<>();
public static synchronized void staticCs() {
System.out.println("SharedResource.staticCs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.staticCs end " + Thread.currentThread());
}
}
실행 결과를 보면, 클래스 레벨에서 모니터에 의해 동기화가 제공되는것을 확인할 수 있다.

인스턴스락과, 클래스락은 따로 동작한다.
인스턴스의 모니터와, 클래스의 모니터는 별개이기 때문에 서로 영향을 주지 않는다. 즉 클래스레벨에서 락이 걸려도 인스턴스에서 락이 걸리지 않았다면, 실행이 가능하다.
아래 예제에서는 클래스레벨에서 모니터 소유권을 얻어 작업을 수행중일때 인스턴스 모니터 소유권을 얻어 동시에 작업을 수행하게 된다.
// code -3
public class Main {
volatile static int a = 0;
public static void main(String[] args) throws Exception {
SharedResource sr = new SharedResource();
Thread t1 = new Thread(() -> SharedResource.staticCs());
Thread t2 = new Thread(() -> sr.cs());
t1.start();
t2.start();
}
}
class SharedResource {
static private List<Integer> staticList = new ArrayList<>();
public synchronized void cs() {
System.out.println("SharedResource.cs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
staticList.add(0, 1);
}
System.out.println("SharedResource.cs end " + Thread.currentThread());
}
public static synchronized void staticCs() {
System.out.println("SharedResource.staticCs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
staticList.add(0, 1);
}
System.out.println("SharedResource.staticCs end " + Thread.currentThread());
}
}

synchronized 블럭은 모니터로 사용할 인스턴스, 클래스를 직접 지정할 수 있다. 해당 블럭은 지정한 모니터 객체, 클래스의 모니터 의해 보호된다. 즉 하나의 인스턴스 더라도 각기 다른 모니터 객체를 사용해 synchronized 블럭을 구성한다면, 각 모니터 객체는 자신의 모니터 영역만 보호하고 서로의 영역은 보호하지 않는다.
아래 예시를 보면 SharedResource 클래스 내부에 lock1 lock2 가 존재한다. 그리고 cs() 메서드는 lock1 을 모니터 객체로 사용해 보호하고, cs2() 메서드는 lock2를 모니터 객체를 사용해 보호한다.
이때 lock1 과 lock2 의 모니터는 상호 영향을 주지 않기 때문에 동일한 인스턴스더라도 두개의 스레드가 동시에 실행된다.
// code - 4
public class Main {
volatile static int a = 0;
public static void main(String[] args) throws Exception {
SharedResource sr = new SharedResource();
Thread t1 = new Thread(() -> sr.cs());
Thread t2 = new Thread(() -> sr.cs2());
t1.start();
t2.start();
}
}
class SharedResource {
public Object lock1 = new Object();
public Object lock2 = new Object();
private List<Integer> list = new ArrayList<>();
public void cs() {
synchronized (lock1) {
System.out.println("SharedResource.cs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.cs end " + Thread.currentThread());
}
}
public void cs2() {
synchronized (lock2) {
System.out.println("SharedResource.cs2 start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.cs2 end " + Thread.currentThread());
}
}
}

만약 두 synchronized 블럭에서 동일한 동기화 객체를 사용한다면, 상호배제를 달성할 수 있다. 또한 this 와 같이 인스턴스 자체를 동기화 객체로 사용한다면 동기화 메서드와 같이 인스턴스 레벨에 락을 걸 수 있다.
정적 메서드에서 synchronized 블럭을 사용한다면 당연하게도 인스턴스(this)를 사용할 수 없다. 이때는 접근 가능한 객체를 사용하거나, 클래스 자체를 synchronized 블럭의 모니터로 사용할 수 있다.
아래 예시에서는 SharedResource.class 를 사용하거나, static field인 lock 을 사용하는 예제다. 물론 두 블럭은 다른 모니터에 의해 보호되기 때문에 상호배제를 달성할 수 없다.
// code - 5
class SharedResource {
public static Object lock = new Object();
private static List<Integer> list = new ArrayList<>();
public static void cs() {
synchronized (SharedResource.class) {
System.out.println("SharedResource.cs start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.cs end " + Thread.currentThread());
}
}
public static void cs2() {
synchronized (lock) {
System.out.println("SharedResource.cs2 start " + Thread.currentThread());
for (int i = 0; i < 200_000; i++) {
list.add(0, 1);
}
System.out.println("SharedResource.cs2 end " + Thread.currentThread());
}
}
}
모니터의 협력을 위한 메서드로 Object 클래스 에 정의되어 있다. 모니터의 소유권을 제어하기 위한 메서드로 쓰레드가 모니터의 소유권을 가질때 즉 syhcnronized 내부에서만 사용할 수 있다. (메서드든 블럭이든 상관 없음)
wait() 은 스레드가 모니터의 소유권을 반납하고 Wait Set 에서 대기하며 다른 스레드가 깨워주기를 기다린다. 모니터의 소유권을 반납하는것이기 때문에 당연히 synchronized 내부에서만 호출이 가능하다.
인스턴스 synchronized 메서드 내부에서는 호출된 메서드의 인스턴스의 wait() 을 호출하게 된다.
호출하게 되면 해당 메서드를 실행중이던 스레드가 Wait Set 으로 이동하고 Entry Set 에서 대기중인 다음 스레드가 모니터 영역에 진입하게 된다.
이때 wait() 이 호출되는 객체의 대상은 당연히 메서드의 인스턴스다.
정적 synchronized 메서드 내부에서 호출은 권장되지 않는다. wait() 의 호출은 객체의 모니터를 소유한 스레드에서 호출해야하는데, 정적 메서드의 경우 객체 없이 호출되고 락 또한 클래스 레벨로 관리된다.
한마디로 스레드가 소유한 객체 모니터 자체가 없기 때문에 wait() 의 호출이 권장되지 않는다.
동기화 블럭은 일단 인스턴스락을 걸지 않는다. 동기화 블럭의 모니터 객체로 인스턴스를 전달하면 락을 걸지만, 그렇지 않다면 전달받은 모니터 객체에 대한 락을 건다.
즉 해당 스레드는 모니터 객체 모니터의 소유권을 가지고 있지, 인스턴스 모니터의 소유권을 가지고 있지 않는다.
이때 인스턴스에 대한 wait() 을 호출한다면, 현재 스레드가 모니터의 소유자가 아니라는 예외가 발생하게 된다.
아래 예제에서는 synchronized 블럭이 인스턴스를 모니터 객체로 쓰지 않고 다른 객체를 모니터 객체로 사용했다. 이상황에서 wait() 을 호출하게 된다면 스레드는 해당 인스턴스 모니터의 소유자가 아니기 떄문에 예외가 발생하게 된다.
// code - 6
class SharedResource {
public static Object lock1 = new Object();
public void cs5 () {
synchronized (lock1) {
System.out.println("SharedResource.cs5 start " + Thread.currentThread());
try {
wait();
} catch (Exception e) {
System.err.println(e);
}
System.out.println("SharedResource.cs5 end " + Thread.currentThread());
}
}
}

wait() 을 스레드가 소유권을 얻은 객체인 lock1.wait() 으로 변경하면 예외없이 동작한다.
정적 메서드도 원리는 동일하게 스레드가 소유권을 얻은 모니터의 객체, 즉 synchronized 블록에 사용된 객체의 wait() 을 호출해야 한다.
synchronized 블럭은 객체 뿐만 아니라 클래스또한 받을 수 있다.
하지만 클래스를 받은 경우 락의 주체(스레드가 소유한 모니터와 연관된)가 객체가 아니기 때문에 wait()을 호출할 수 없다. 이경우는 인스턴스 메서드에서 클래스를 사용해 synchronized 블럭을 구성해도 동일하다.
notify() 는 같은 모니터(같은 객체)의 Wait Set 에서 대기중인 스레드를 깨우는 메서드다. 대기중인 스레드들 중에서 하나의 스레드만 깨우게 된다.
이때 notify를 하더라도 모니터의 소유권을 release하는것은 아님에 주의해야 한다.
notify() 는 Wait Set 의 스레드중 하나만 깨우기 때문에 우선순위가 낮은 스레드의 경우 우선순위가 높은 스레드에 밀려 영원히 깨어나지 못하는 기아 현상이 발생할 수 있다. 때문에 후술할 notifyAll() 의 사용이 권장된다.
notify() 는 같은 모니터(같은 객체)의 Wait Set 에서 대기중인 모든 스레드를 깨우는 메서드다. 모든 스레드를 깨우기 때문에 기아 상태에 빠질 가능성이 낮아진다. 모든 스레드를 깨우지만, 모든 스레드가 모니터의 소유권을 얻는것은 아니다. 즉 자기들끼리 경쟁을해 (race condition) 이긴 스레드 하나만이 모니터의 소유권을 갖게 된다.
이때도 물론, notifyAll()를 호출한 스레드가 모니터의 소유권을 release 하는것은 아니다.
synchronized 키워드는 모니터를 통해 동기화 문제를 해결한다.
인스턴스 synchronized 메서드 는 인스턴스의 모니터를 통해 상호배제를 달성하기 때문에 동일 인스턴스의synchronized 메서드 에 대한 동기화를 제공해준다.정적 synchronized 메서드 는 클래스의 모니터를 통해 상호배제를 달성하기 때문에 동일 클래스의 정적 synchronized 메서드에 대한 동기화를 제공한다. synchronized 블럭 은 블럭에 전달된 객체, 클래스의 모니터를 통해 상호배제를 달성한다. 때문에 인스턴스 메서드에서 사용한다면 this 를 통해 해당 인스턴스에 대한 동기화를 할 수 있고, Class.class 로 클래스 레벨의 동기화 또한 가능하다.wait(), notify(), notifyAll() 은 모니터의 소유권을 스레드끼리 나눠가지며 협력을 제공해준다. Object 클래스에 정의된 메서드이기 때문에 객체 없이는 실행할 수 없다.