Spring boot 환경에서 스케줄러를 동적으로 구현하는 중에 일부 스레드가 중복 실행되는 현상이 발생
스케줄러에 등록, 삭제는 정상적으로 작동하고 생성되는 인스턴스도 문제가 없었서 원인을 해결 못하고 synchronized 를 사용해서 막아버리기로 결정
그래서 간단히 정리했습니다
- 여러 개의 스레드가 한 개의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 스레드를 제외하고 나머지는 데이터에 접근 할 수 없도록 막는 개념
- 메서드 또는 코드 블록을 임계 영역으로 지정 가능
- 임계 영역 : 공유되는 자원, 즉 동시 접근하려고 하는 그 자원에서 문제가 발생하지 않게 독점을 보장해줘야 하는 영역
- 스레드는 해당 영역에 접근하기 위해 모니터 락을 획득해야 함, 다른 스레드가 이미 해당 영역에 대한 모니터 락을 가지고 있으면, 해당 스레드는 락이 해제될 때까지 대기
- 모니터 락 : 뮤텍스나 세마포어보다 더 고수준의 동기화 기법
- 락은 메서드 단위로 설정할 수 있고, 특정 코드 블록에만 설정할 수도 있음
- 락을 얻는 작업만으로도 오버헤드가 발생하므로, 복잡한 계산, 네트워크 작업, 사용자 입출력 작업 등 시간이 오래 걸리는 작업은 가능한 임계 영역에서 제외하는 것이 좋음
다중 스레드에서 동기화 없으면 발생하는 문제
public class Test{
@Test
public void testMethod() {
Task task = new Task();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
class Counter {
int count = 5000;
public void decrement() {
if(count > 0) {
count = count - 100;
System.out.println("Thread : " + Thread.currentThread() + ", Count : " + count);
}
}
}
class Task implements Runnable {
Counter c = new Counter();
@Override
public void run() {
while(c.count > 0) {
c.decrement();
}
}
}
}

- 마지막 결과를 확인하면 Thread-1.5 는 이미 Count가 0인데도 계산 작업을 이어 감
- Thread-0.5 와 Thread-1.5 는 서로 경쟁하며 Counter 의 자원에 접근을 하게 되고, Thread-0.5 가 작업을 마치고 결과를 반환하기 전에 Thread-1.5 가 Counter 의 자원에 접근을 하게 돼서 예상할 수 없는 결과가 나오게 됨
- 접근을 임의로 하는 것이 아닌 처음 접근한 스레드가 먼저 작업을 마치고, 결과를 완전히 반환하고, 다음 스레드가 해당 자원을 접근해야 안전함
공유 자원에 대한 락 적용
- 위 코드와 동일하나 decrement() 메서드에 synchronized 를 적용
public synchronized void decrement() {
if(count > 0) {
count = count - 100;
System.out.println("Thread : " + Thread.currentThread() + ", Count : " + count);
}
}

- synchronized 키워드를 사용해 Counter 의 count 를 사용하는 공유 영역을 thread-safe, 예상 가능한 작업 결과가 나옴
동기화(synchronized) 적용 종류
- 동기화가 필요한 영역의 공유 범위에 따라 달라짐
모니터 락 수준
- 객체 수준의 모니터
- synchronized(this), 인스턴스 메소드 동기화 가 가지는 수준
- 동일한 인스턴스를 사용하는 스레드들은 동시에 접근할 수 없음, 다른 인스턴스에서는 접근 가능
- 같은 인스턴스를 공유하는 경우에 서로 동기화
- 클래스 수준의 모니터
- 스태틱 메소드 동기화, synchronized(Class.class) 가 가지는 수준
- 해당 클래스의 모든 인스턴스가 이 블록에 동시에 접근할 수 없음, 클래스 레벨에서의 동기화가 이루어짐
인스턴스 메소드 동기화
- 해당 메소드가 호출되는 객체 인스턴스의 모니터를 사용하여 동기화 처리,
- 여러 스레드가 동일한 객체의 인스턴스 메소드를 여러 스레드가 동시에 호출할 수없음
- 특정 객체에 대한 동기화, 같은 인스턴스를 사용하는 스레드 간의 충돌 방지
public synchronized void syncMethod() {
/****/
}
스태틱 메소드 동기화
- 해당 클래스의 모니터를 사용하여 동기화를 처리, 해당 클래스의 모든 인스턴스가 이 메소드를 동시에 호출할 수 없음
- 클래스 수준의 공유 데이터에 대해 여러 스레드가 접근하는 경우에 안전하게 사용할 수 있음
- 클래스 수준에서의 동기화, 모든 인스턴스가 공유하는 자원에 대한 충돌 방지
public static synchronized void syncMethod() {
}
블록 동기화
- 특정 코드 블록만 동기화 하는데 사용
- 스레드는 해당 블록에 진입하기 전에 모니터를 획득해야 하고, 다른 스레드는 모니터를 가진 스레드가 블록을 벗어날 때가지 접근할 수 없음
- 인스턴스를 사용하면 객체 모니터, static 필드를 사용하면 클래스 모니터를 가지게 됨
public void objectMontiorMethod() {
synchronized(this) {
}
}
public void classMonitorMethod() {
synchronized(Sample.class) {
}
}
- 성능 최적화
- 동기화의 범위를 줄여 필요한 부분만 동기화 하여 성능을 향상
- 유연성
- 동기화가 필요한 코드만 선택적으로 보호할 수 있음
주의 사항
- 동기화를 적용하려는 영역의 범위를 잘 알고, 적절히 동기화를 수행해야 함
- 블록 동기화를 사용하여 세밀한 범위를 조절할 수 있어 좋지만, 너무 많은 단위 블럭을 생성하게 되면 오버헤드를 초래하므로 주의
- DeadLock(교착 상태) 에 빠질 수 있으므로 주의 필요