Java 동기화

이태형·2024년 11월 23일

Growth Log

목록 보기
6/18

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(교착 상태) 에 빠질 수 있으므로 주의 필요

0개의 댓글