[CS_study] Synchronized 키워드에 대해 알아보자

JUN·2024년 11월 15일
0

CS

목록 보기
2/4
post-thumbnail

Java에서 멀티스레드 프로그래밍을 하다 보면 동기화 문제가 자주 발생한다.

이때 Synchronized는 공유자원을 생성하여 해당 자원에 대한 동기화 영역을 생성하는데 도움을 준다.

1. Synchronized 키워드란?

Synchronized : 자바에서 동기화 영역을 생성하는 키워드이다.

Synchrosized 처리된 객체나 메서드는 두 개 이상의 쓰레드가 동시 접근하는 것을 막는다.

즉 하나의 스레드가 해당 객체나 메서드를 사용하는 동안 다른 스레드가 접근하지 못하도록 Lock을 거는 것.

그런데 두 개 이상의 스레드가 synchronized 처리된 객체나 메서드에 동시에 접근을 시도할때 어떤 스레드가 우선순위를 가지게 될까?

Java 스레드 스케줄러가 다음과 같은 원칙에 따라 어떤 스레드가 먼저 실행할지를 결정한다.

  1. 스레드 우선순위
    • 자바 스레드는 우선순위를 가질 수 있음.
    • 우선순위가 높은 스레드가 낮은 스레드보다 실행될 가능성이 큼.
  2. JVM 스케줄링 정책
    • JVM 스레드 스케줄러가 운영체제의 스케줄러와 협력하여 실행 순서를 결정함.
    • 타임 슬라이싱(Time Slicing)과 우선순위 기반 스케줄링 방식이 일반적.
  3. Fairness 옵션
    • ReentrantLock 클래스의 공정성 설정을 통해 대기 순서대로 접근 제어 가능.
      ReentrantLock lock = new ReentrantLock(true); // 공정성 설정

2. Synchronized 키워드 사용 위치별 동기화 방식 비교

1. 인스턴스 메소드 동기화

  • 설명: synchronized 키워드를 메소드 선언에 붙이면, 해당 메소드를 호출한 인스턴스(객체)를 기준으로 동기화가 이루어진다.
  • 특징:
    • 한 인스턴스에서 한 번에 한 스레드만 메소드를 실행할 수 있다.
    • 다른 non-synchronized 메소드는 동시에 실행될 수 있다.
  • 예제:
    public synchronized void add(int value) {
        this.count += value;
    }
    
  • 사용 시 주의점:
    • 객체당 한 스레드만 메소드를 실행하도록 보장한다.
    • 클래스의 여러 인스턴스는 서로 독립적으로 동작한다.

2. 스태틱 메소드 동기화

  • 설명: static synchronized 메소드는 해당 클래스 자체를 기준으로 동기화가 이루어진다.
  • 특징:
    • 클래스 레벨에서 동기화가 이루어진다.
    • 해당 클래스의 모든 인스턴스가 공유하는 리소스에 대한 동기화가 필요할 때 사용한다.
    • 한 번에 한 스레드만 static synchronized 메소드를 실행할 수 있다.
  • 예제:
    public static synchronized void add(int value) {
        count += value;
    }
    
  • 사용 시 주의점:
    • 클래스 객체는 JVM 내에서 하나만 존재하므로, 클래스 간 자원을 보호해야 할 때 적합하다.

3. 인스턴스 메소드 내부 블록 동기화

  • 설명: 메소드 내부의 특정 코드 블록을 synchronized 키워드로 동기화할 수 있다.
  • 특징:
    • 동기화가 필요한 코드만 블록으로 지정할 수 있다.
    • 동기화 기준은 this 객체(메소드를 호출한 객체) 또는 다른 객체로 설정할 수 있다.
  • 예제:
    public void add(int value) {
        // 동기화되지 않은 코드
        synchronized(this) {
            this.count += value; // 이 부분만 동기화
        }
        // 동기화되지 않은 코드
    }
    
  • 사용 시 주의점:
    • 메소드 전체 동기화보다 더 세밀한 동기화가 가능하다.
    • 다른 객체를 기준으로 동기화하면, 여러 스레드 간 독립적인 동작이 가능하다.

4. 스태틱 메소드 내부 블록 동기화

  • 설명: 스태틱 메소드 내부의 특정 코드 블록을 동기화하며, 클래스 객체를 기준으로 동기화가 이루어진다.
  • 특징:
    • 동기화 기준은 클래스 객체 (ClassName.class)이다.
    • 클래스의 모든 스레드에 대해 동기화를 보장한다.
  • 예제:
    public static void add(int value) {
        synchronized (MyClass.class) {
            count += value; // 이 부분만 동기화
        }
    }
    
  • 사용 시 주의점:
    • 클래스의 다른 스태틱 메소드와 동시 실행을 방지할 수 있다.

종합 비교

유형동기화 기준동작 범위특징
인스턴스 메소드호출한 객체 (this)메소드 전체객체 단위로 동기화되며, 한 객체당 한 스레드만 실행 가능하다.
스태틱 메소드클래스 객체메소드 전체클래스 단위로 동기화되며, 모든 인스턴스가 공유한다.
인스턴스 메소드 블록호출한 객체 (this)특정 코드 블록객체 단위로 동기화되며, 블록 내 코드만 한 번에 한 스레드만 실행 가능하다.
스태틱 메소드 블록클래스 객체특정 코드 블록클래스 단위로 동기화되며, 특정 블록만 한 번에 한 스레드만 실행 가능하다.

3. Synchronized의 효율성

Synchronized는 데이터의 일관성을 보장해 주지만, 코드의 실행 시간이 길어질수록 다른 스레드의 대기 시간이 늘어나면서 시스템의 효율성이 떨어질 수 있다.

따라서 공유 객체를 사용하는 임계 영역(critical section)은 꼭 필요한 부분에만 최대한 작게 유지하는 것이 중요하다.

CleanCode 13.동시성

p236, 동기화하는 부분을 최대한 작게 만들어라.임계 영역을 줄인다고 임계 영역의 크기를 키우지는 마라. 그러면 스레드간의 경쟁도 늘고 성능도 떨어진다.

4. Synchronized 의 대체 기법.

Java 5부터는 스레드 안전한 java.util.concurrent 패키지가 제공되면서 보다 정교한 동기화 제어가 가능해졌다.

ReentrantLock, ReadWriteLock, Semaphore, Atomic 클래스들이 대표적이다.

  • ReentrantLock
    • synchronized보다 더 세밀한 제어 가능
    • 락의 획득과 해제를 명시적으로 관리
    • 공정성 설정 가능
  • ReadWriteLock
    • 읽기와 쓰기를 구분하여 동기화 적용
    • 다수의 스레드가 동시에 읽기 가능
    • 쓰기 작업 시에만 쓰기 락 적용
  • Semaphore
    • 스레드 수를 제어
    • 특정 리소스에 접근 가능한 최대 스레드 수 제한
  • Atomic 클래스들 (AtomicInteger, AtomicLong 등)
    • 원자적인 연산 제공
    • 동기화 없이 안전한 연산 수행 가능

CleanCode 13.동시성

p233, 언어가 제공하는 스레드 안전한 클래스를 검토하고 자바에서는 해당 클래스들을 익혀라.

5. Thread Local?

Synchronize 가 스레드의 공유자원을 설정하는 키워드라면 ThreadLocal은 Java에서 각 스레드가 독립적으로 자원을 보유할 수 있도록 도와주는 클래스이다.

ThreadLocal 사용법

  1. ThreadLocal 객체 생성

    private ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
    

    각 스레드는 이 객체를 통해 독립된 값을 설정하고 가져올 수 있다.

  2. 값 설정

    myThreadLocal.set("Thread-specific value");
    

    현재 스레드에 특정 값을 저장한다.

  3. 값 가져오기

    String value = myThreadLocal.get();
    

    현재 스레드에 설정된 값을 반환한다.

  4. 값 제거

    myThreadLocal.remove();
    

    현재 스레드의 값을 초기화한다.


특징

  • 독립적인 변수 관리: 각 스레드가 독립적으로 값을 보유하므로 변수 간섭이 없다.
  • 초기값 설정 가능: ThreadLocalinitialValue() 메소드를 오버라이드하여 기본값을 제공할 수 있다.
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    

InheritableThreadLocal

  • 기본적으로 ThreadLocal은 부모 스레드의 값을 자식 스레드가 참조하지 못한다.
  • InheritableThreadLocal을 사용하면 부모 스레드의 값을 자식 스레드에 전달할 수 있다.
    private InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    
  • 부모 스레드의 초기값을 자식 스레드가 물려받지만, 자식 스레드가 값을 변경해도 부모 스레드에는 영향을 미치지 않는다.

장점

  1. 스레드 안전성: 각 스레드가 고유한 값을 보유한다.
  2. 사용 편의성: 스레드별로 데이터를 저장하고 관리하는 코드를 간결하게 작성할 수 있다.
  3. 유연성: initialValue()InheritableThreadLocal을 활용해 초기값과 상속 관계도 커스텀하여 설정 가능하다.

사용 예시

  1. 각 스레드에 고유 값 부여

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    
    public void increment() {
        threadLocal.set(threadLocal.get() + 1);
    }
    
    public Integer getValue() {
        return threadLocal.get();
    }
    
  2. InheritableThreadLocal 사용

    private InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    
    public void setParentValue(String value) {
        threadLocal.set(value);
    }
    
    public String getValue() {
        return threadLocal.get();
    }
    

주의점

  1. 메모리 누수: 스레드가 종료되면 ThreadLocal에 저장된 값을 반드시 remove()로 제거해야 한다.
  2. ThreadLocal 변수 관리: 관리되지 않는 ThreadLocal 객체는 값이 참조되지 않더라도 GC에 의해 수거되지 않을 수 있다.

참고

profile
순간은 기록하고 반복은 단순화하자 🚀

0개의 댓글