[Java] Synchronized 키워드를 알아보겠습니다.
Synchronized 키워드
Java 는 synchronized 키워드를 사용하여 멀티 스레드 환경에서 스레드 간 동기화와 상호 배제를 보장하기 위해 모니터 락(Monitor Lock)을 내부적으로 사용합니다. 이 락은 하나의 스레드만 임계 구역에 접근하도록 하여 데이터의 무결성을 보장합니다.
이 과정에서 JVM 은 다음 작업을 수행합니다.
- 락을 얻기 위한 경쟁 처리
- 락 상태 업데이트
- 락 소유자가 해제될 때 다른 대기 중인 스레드에게 락을 전달
synchronized 키워드는 다음과 같은 경우에 사용할 수 있습니다.
- synchronized method
- static synchronized method
- synchronized block
1. Synchronized method
Synchronized Method는 인스턴스 단위로 락을 걸어 synchronized가 선언된 메서드 실행 중 다른 스레드가 해당 인스턴스의 synchronized 메서드에 접근하지 못하도록 제한합니다.
예제 1 : 하나의 인스턴스를 공유하는 경우
public class Method {
public static void main(String[] args) {
Method method1 = new Method(); //인스턴스 생성
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
method1.syncMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 : " + LocalDateTime.now());
method1.syncMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중 " + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중 " + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
실행 결과 :
스레드1 시작 2025-01-03T16:02:25.561005
스레드2 시작 : 2025-01-03T16:02:25.560922
스레드1의 syncMethod1 실행중 2025-01-03T16:02:25.563279
스레드1 종료 2025-01-03T16:02:28.574908
스레드2의 syncMethod2 실행중 2025-01-03T16:02:28.574908
스레드2 종료 2025-01-03T16:02:31.577862
syncMethod1과 syncMethod2는 같은 객체(method1)에 대해 락을 공유하므로, thread1이 메서드를 종료하기 전까지 thread2는 대기합니다.
예제 2 : 서로 다른 객체를 참조하는 경우
public class Method {
public static void main(String[] args) {
Method method1 = new Method();
Method method2 = new Method(); // 별도 인스턴스 생성
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
method1.syncMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
method2.syncMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
}
실행 결과 :
스레드1 시작 2025-01-03T16:03:45.752625
스레드2 시작 : 2025-01-03T16:03:45.752969
스레드2의 syncMethod2 실행중 2025-01-03T16:03:45.755568
스레드1의 syncMethod1 실행중 2025-01-03T16:03:45.755413
스레드1 종료 2025-01-03T16:03:48.774111
스레드2 종료 2025-01-03T16:03:48.773879
method1과 method2는 서로 다른 객체이므로 락이 공유되지 않습니다. 따라서 두 스레드가 동시에 실행됩니다.
인스턴스 단위로 락을 공유하지만, synchronized 키워드가 붙은 메소드에 대해서만 lock 을 공유합니다.
예제3 : 하나의 인스턴스가 synchronized 가 붙은 메소드를 호출하는 스레드들과 일반 메소드를 호출하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class Method {
public static void main(String[] args) {
Method method = new Method();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
method.syncMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
method.syncMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
Thread thread3 = new Thread(() -> {
System.out.println("스레드3 시작 " + LocalDateTime.now());
method.method3("스레드3");
System.out.println("스레드3 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
thread3.start();
}
private synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void method3(String msg) {
System.out.println(msg + "의 method3 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
결과 :
스레드2 시작 2025-01-03T16:07:21.200069
스레드3 시작 2025-01-03T16:07:21.200079
스레드1 시작 2025-01-03T16:07:21.200314
스레드2의 syncMethod2 실행중2025-01-03T16:07:21.203064
스레드3의 method3 실행중2025-01-03T16:07:21.203124
스레드3 종료 2025-01-03T16:07:26.222513
스레드2 종료 2025-01-03T16:07:26.222513
스레드1의 syncMethod1 실행중2025-01-03T16:07:26.222513
스레드1 종료 2025-01-03T16:07:31.228128
하나의 객체에서 여러 스레드가 synchronized 메서드를 호출하면 락이 걸려 대기합니다. 하지만, 일반 메서드는 락이 걸리지 않아 동시에 실행되는 것을 알 수 있습니다.
2. static synchronized method
static 키워드가 포함된 synchronized 메소드는 인스턴스가 아닌 클래스 단위로 lock 을 공유합니다.
예제 1 : static synchronized method 가 붙은 메소드를 여러 스레드가 실행하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class StaticMethod {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
syncStaticMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
syncStaticMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start(); }
public static synchronized void syncStaticMethod1(String msg) {
System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void syncStaticMethod2(String msg) {
System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
실행 결과 :
스레드1 시작 2025-01-03T16:16:06.664
스레드2 시작 2025-01-03T16:16:06.664269
스레드1의 syncStaticMethod1 실행중 2025-01-03T16:16:06.665145
스레드1 종료 2025-01-03T16:16:11.679765
스레드2의 syncStaticMethod2 실행중 2025-01-03T16:16:11.679779
스레드2 종료 2025-01-03T16:16:16.683827
syncStaticMethod1과 syncStaticMethod2는 클래스 단위로 락을 공유하므로, 한 메서드가 실행 중이면 다른 메서드는 대기합니다.
클래스 단위에 거는 lock 과 인스턴스 단위에 거는 lock 은 공유가 되지 않기 때문에 혼용해서 사용하면 동기화 이슈가 발생하게 됩니다.
예제 2 : static synchronized method 와 synchronized method 를 혼용하는 경우
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class StaticMethod {
public static void main(String[] args) {
StaticMethod method = new StaticMethod();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
syncStaticMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
syncStaticMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
Thread thread3 = new Thread(() -> {
System.out.println("스레드3 시작 " + LocalDateTime.now());
method.syncMethod3("스레드3");
System.out.println("스레드3 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
thread3.start();
}
public static synchronized void syncStaticMethod1(String msg) {
System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void syncStaticMethod2(String msg) {
System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void syncMethod3(String msg) {
System.out.println(msg + "의 StaticMethod3 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
결과:
스레드3 시작 2025-01-03T16:16:06.664
스레드2 시작 2025-01-03T16:16:06.664269
스레드1 시작 2025-01-03T16:16:06.663121
스레드2의 syncStaticMethod2 실행중2025-01-03T16:16:06.665209
스레드3의 StaticMethod3 실행중2025-01-03T16:16:06.665145
스레드2 종료 2025-01-03T16:16:11.679765
스레드1의 syncStaticMethod1 실행중2025-01-03T16:16:11.679779
스레드3 종료 2025-01-03T16:16:11.682843
스레드1 종료 2025-01-03T16:16:16.683827
일반 synchronized 메소드를 추가하면, static synchronized 메소드를 사용하는 스레드1 과 스레드2 간에는 동기화가 잘 지켜지는 것을 알 수 있습니다. 그러나 synchronzied 메소드를 사용한 스레드3은 개발자가 의도한 동기화가 잘 지켜지지 않았습니다.
3. synchronized block
인스턴스의 block 단위로 lock 을 걸며, 2가지의 사용방법이 있습니다.
- synchronized(this)
- synchronized(Object)
Synchronized(this) 를 사용하면, 모든 synchronized block 에 lock 이 걸립니다.
예제 1: Synchronized(this) 를 사용하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class Block1 {
public static void main(String[] args) {
Block1 block = new Block1();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
block.syncBlockMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
block.syncBlockMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private void syncBlockMethod1(String msg) {
synchronized (this) {
System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void syncBlockMethod2(String msg) {
synchronized (this) {
System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
결과 :
스레드1 시작 2025-01-03T16:26:08.656798
스레드2 시작 2025-01-03T16:26:08.656862
스레드1의 method1 실행중 2025-01-03T16:26:08.659802
스레드1 종료 2025-01-03T16:26:11.678866
스레드2의 method2 실행중 2025-01-03T16:26:11.678865
스레드2 종료 2025-01-03T16:26:14.680457
여러 스레드가 들어와서 서로 다른 synchronized block 을 호출해도 this 를 사용해 자기 자신에 lock 을 걸었기 때문에 인스턴스 자체에 락을 걸어 모든 synchronized 블록을 동기화합니다. 그러나 위 방식은 모든 블럭에 lock 이 걸리기 때문에 상황에 따라 비효율적일 수 있습니다.
synchronized(Object) 를 사용하여 블록마다 다른 lock 이 걸리게 할 수 있습니다.
예제 2: synchronized(Object)
public class Block2 {
private final Object o1 = new Object();
private final Object o2 = new Object();
public static void main(String[] args) {
Block2 block = new Block2();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
block.syncBlockMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
block.syncBlockMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private void syncBlockMethod1(String msg) {
synchronized (o1) {
System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void syncBlockMethod2(String msg) {
synchronized (o2) {
System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
결과 :
스레드1 시작 2025-01-03T16:29:24.697035
스레드2 시작 2025-01-03T16:29:24.696902
스레드2의 syncBlockMethod2 실행중2025-01-03T16:29:24.699335
스레드1의 syncBlockMethod1 실행중2025-01-03T16:29:24.699075
스레드1 종료 2025-01-03T16:29:25.719566
스레드2 종료 2025-01-03T16:29:25.718522
스레드1과 스레드2간의 동기화가 지켜지지 않은 것을 확인할 수 있습니다. 따라서 this 가 아닌 o1 과 o2 객체를 만들어 인자로 넘겨주면 동시에 lock 이 걸려야 하는 부분을 따로 지정해줄 수 있습니다.
위의 내용을 정리하자면 다음과 같습니다.
- synchronized method 는 인스턴스 단위로 락을 공유합니다.
- 이 때, synchronized 키워드가 붙은 메소드에 대해서만 락이 걸립니다. (일반 메서드는 동기화가 되지 않습니다.)
- static synchronized method 는 클래스 단위로 락을 공유합니다.
- static synchronized method 와 synchronized method 를 혼용하면, 동시성 문제가 야기할 수 있습니다.
- synchronized block 은 두 가지 방법으로 사용할 수 있습니다.
- synchronized(this) 를 사용하는 경우 모든 synchronized block 에 락이 걸립니다.
- synchronized(Object) 를 사용하는 경우 동시에 lock 이 걸려야 하는 부분을 따로 지정할 수 있습니다.
참조
https://github.com/jvm-hater/java-study/blob/main/2%EC%A3%BC%EC%B0%A8/synchronized.md