반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 빈 검증
  • item7
  • 알고리즘
  • next js app
  • 이펙티브자바
  • MSSQL
  • 자바
  • 참조 해제
  • 싱글턴
  • 이펙티브 자바
  • try-with-resources
  • 추상 팩터리 패턴
  • bean Validation
  • 싱글톤
  • item8
  • serverless computing
  • NEXT JS
  • 자바스크립트 인터뷰
  • 플라이웨이트패턴
  • 자바스크립트 면접
  • 메이븐 라이프사이클
  • Javadoc 자바독 자바주석 주석 Comment
  • 외래키 제약조건
  • 스프링 검증
  • prerendering
  • 러스트
  • 자바 디자인패턴
  • 메이븐 골
  • 느린 쿼리
  • pnpm
  • 디자인패턴
  • Next.js
  • 팩터리 메서드 패턴
  • Pre-rendering
  • 도커공식문서
  • 자바 검증
  • 슬로우 쿼리
  • 객체복사
  • Java
  • 서버리스 컴퓨팅
  • 프로그래머의 뇌
  • 메이븐 페이즈
  • 토비의 스프링
  • 이펙티브 자바 item9
  • rust
  • 싱글톤 패턴
  • item9
  • 자바스크립트
  • 작업기억공간
  • 자료구조

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

Java/자바 잡지식

자바의 synchronized 키워드 정복하기

2022. 4. 3. 23:09

주로 해결하려는 문제: 동시성 문제에서의 경쟁 상태

  • 멀티 스레드 프로그래밍에서의 경쟁 상태를 해결하기 위해서 필요하다.
    • 경쟁 상태란 여러 스레드가 순서에 영향을 받는 작업을 병렬적으로 할 때, 의도치 않은 결과가 나오는 것을 말한다.
  • 비동기 프로그래밍에서 각 동작의 시작과 끝이 보장되지 않으면 실행/출력 결과가 일정하지 않을 수 있고 의도와 다른 결과물을 보여줄 수 있으며 심각한 버그까지 초래할 수 있다.
    • ex) object.method() 는 Thread1 완벽히 수행을 끝낸 이후에 Thread2 가 수행해야 한다. 행위에 대한 Atomic 함이 보장되어야 한다.

보통 자바 프로그래밍에서 멀티 스레드 프로그래밍 코드를 작성해 볼 일은 잘 없다고 생각하지만, 엔터프라이즈 서버 환경을 경험해볼 일은 매우 많다. 자바를 배우는 대부분은 스프링을 이용해 돈을 번다.

엔터프라이즈 서버 환경에서는 사용자 요청을 스레드 풀에 있는 다수의 스레드가 처리하는 경우가 많다. 서버는 다수의 스레드를 이용해 멀티 스레드를 기반으로 한 동작을 하기 때문에 대충 작성한 코드가 생각지도 못한 결과로 나타날 때가 있기 때문에 주의해야 한다.

문제가 되는 예시: 달걀을 좋아하는 가족

  • 달걀을 잘 먹는 한 가족이 있다.
  • 이 가족의 구성원은 엄마, 아빠, 아들이다.
  • 이 가족은 달걀을 좋아한다.
  • 그래서 주방 한가운데 달걀 바구니가 있다.
  • 가족은 늘 달걀 바구니에서 달걀을 꺼내먹는다.
  • 달걀 바구니에 달걀이 없으면 달걀을 1판씩 사오기로 약속했다.
  • 아빠는 달걀 바구니가 빈 것을 보고 달걀을 사러 갔다.
  • 아들은 아빠가 달걀을 사러 간 것을 모르고 또 달걀을 사러 갔다.
  • 집에 오니 엄마가 산 달걀까지 있어 총 3판의 달걀이 쌓였다.

가족은 달걀 3판을 유통기한 내에 먹을 수 있을지 고민이다.

예시 문제 해결

  • 달걀을 사러 가는 사람이 달걀 바구니를 같이 들고간다면?
    • 다른 가족은 달걀을 사러간 동안 달걀 바구니에 접근할 수 없다.
    • 달걀 바구니가 없는 것을 보고 누군가 달걀을 사러 갔겠거니 생각할 수 있다.

문제가 되는 예시: Counter 코드

int count = 0;

@Test
public void nonAtomicCountTest() throws InterruptedException {
    Runnable runnable1 = () -> {
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    };

    Runnable runnable2 = () -> {
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    };

    Thread thread1 = new Thread(runnable1);
    Thread thread2 = new Thread(runnable2);

    thread1.start();
    thread2.start();

    Thread.sleep(1000);

    System.out.println("count = " + count);
}

위의 코드를 실행하면 count 값으로 몇이 출력될까? 얼핏보면 당연히 200000 이 출력될 것 같다. 정답은 컴퓨터마다 다르겠지만, 20만보다 작은 랜덤한 값이 출력된다.

그 이유를 알아보려면 먼저, count++의 동작부터 자세히 알아야 한다.

count++는 겉보기에는 1개의 operation 처럼 보이지만, 사실은 3개의 operation 으로 이루어져있다. temp = count, temp = temp + 1, count = temp 와 같이 총 3가지의 연산으로 구성되어 있다.

다음으로는 스레드가 병렬처리를 하고 있다는 것을 인지해야 한다. 두 스레드는 temp = count, temp = temp + 1, count = temp 를 반복적으로 수행중이다.

count = 100000 일 때 thread1과 thread2의 동작을 가정해보자.

  • thread1: temp1 = count -> temp1:100000, count:100000
  • thread2: temp2 = count -> temp2:100000, count:100000
  • thread1: temp1 = temp1 + 1 -> temp1:100001, count:100000
  • thread1: count = temp1 -> temp1:100001, count:100001
  • thread1: temp1 = count -> temp1:100001, count:100001
  • thread1: temp1 = temp1 + 1 -> temp1:100002, count:100001
  • thread1: count = temp1 -> temp1:100001, count:100002
  • thread2: temp2 = temp2 + 1 -> temp2:100001, count:100002
  • thread2: count = temp2 -> temp2:100001, count:100001 (?)

thread1과 thread2의 순서가 보장되어있지 않다는 이유 하나만으로 이러한 일이 일어난다. thread1이 이미 올려놓은 값에 thread2가 이전 값을 할당해버린다.

위는 예제이며 정확한 JVM 엔진의 동작인지 검증된 바는 없다.

자바에서는 이러한 문제를 해결하기 위해서 많은 방법을 제공한다.

문제 해결 코드 예시들

문제 해결 코드 1: 메서드 코드 내에서 synchronized 블록문 사용

@Test
public void synchronizedBlockCountTest() throws InterruptedException {
  Runnable runnable1 = () -> {
      for (int i = 0; i < 100000; i++) {
          synchronized (this) {
              count = count + 1;
          }
      }
  };

  Runnable runnable2 = () -> {
      for (int i = 0; i < 100000; i++) {
          synchronized (this) {
              count = count + 1;
          }
      }
  };

  Thread thread1 = new Thread(runnable1);
  Thread thread2 = new Thread(runnable2);

  thread1.start();
  thread2.start();

  Thread.sleep(1000);

  System.out.println("count = " + count);
}
  • 하나의 스레드에서 synchronized 블록 문에 접근할 때는 다른 스레드에서 해당 인스턴스에 동시에 접근할 수 없다.

문제 해결 코드 2: 메서드 시그니처에 synchronized 키워드 사용

@Test
public void synchronizedMethodCountTest() throws InterruptedException {
    Runnable runnable1 = this::syncMethod;
    Runnable runnable2 = this::syncMethod;

    Thread thread1 = new Thread(runnable1);
    Thread thread2 = new Thread(runnable2);

    thread1.start();
    thread2.start();

    Thread.sleep(1000);

    System.out.println("count = " + count);
}

public synchronized void syncMethod() {
    for(int i=0; i<100000; i++) {
        count++;
    }
}
  • 메서드에 synchronized 키워드를 붙여, 멀티 스레드가 동시에 해당 메서드를 수행할 수 없게 한다.

문제 해결 코드 3: 자바가 제공하는 Atomic... 클래스 사용

AtomicInteger atomicCount = new AtomicInteger();

@Test
public void atomicCountTest() throws InterruptedException {
    Runnable runnable1 = () -> {
        for (int i = 0; i < 100000; i++) {
            atomicCount.incrementAndGet();
        }
    };

    Runnable runnable2 = () -> {
        for (int i = 0; i < 100000; i++) {
            atomicCount.incrementAndGet();
        }
    };

    Thread thread1 = new Thread(runnable1);
    Thread thread2 = new Thread(runnable2);

    thread1.start();
    thread2.start();

    Thread.sleep(1000);

    System.out.println("atomicCount = " + atomicCount);
}
  • 자바는 멀티 스레드 프로그래밍을 제공하는만큼 멀티 스레드에서 발생하는 문제를 해결하기 위한 다양한 클래스도 제공한다.
  • Atomic 이 붙은 클래스를 사용하면 동시성 문제에서 안전할 수 있다.

synchronized 블록 자세히 알아보기

  • synchronized 블록에 들어있는 코드는 한번에 하나의 스레드에서만 실행할 수 있다.
  • 한 번에 하나의 스레드만 실행할 수 있기에 경쟁 상태를 피하기 위해 사용될 수 있다.

원리 간단히 알아보기

  • 이는 monitor 라는 것을 통해 구현된다.
  • 자바 오브젝트는 monitor 와 통신한다.
  • 스레드는 monitor 를 lock 하거나 unlock 할 수 있다.
  • 오직 하나의 스레드만 monitor 에 대해 락을 가질 수 있다.
    • 락을 가지려 하는 다른 스레드들은 락이 풀릴 때까지 블록된다.
  • synchronized 블록에서는 호출할 메서드가 있는 오브젝트를 monitor 로 삼고 락을 가진다.

synchronized 블록의 사용법 4가지

  • 인스턴스 메서드에서 사용하기
  • 스태틱 메서드에서 사용하기
  • 인스턴스 메서드 내부의 코드 블록에서 사용하기
  • 스태틱 메서드 내부의 코드 블록에서 사용하기

synchronized 키워드가 걸린 인스턴스 메서드 블록

public class MyCounter {
  private int count = 0;

  public synchronized void add(int value){
      this.count += value;
  }
  public synchronized void subtract(int value){
      this.count -= value;
  }
}
  • 위와 같이 사용하면, 한 스레드 당 하나의 인스턴스에 대한 락을 갖는다.
  • add 와 subtract 에 대한 락은 메서드라고 별도로 취급되는 것이 아니라, 인스턴스별로 하나의 락만 있다.
    • 즉, 스레드가 해당 인스턴스에 대한 락을 가지고 있을 때, 모든 메서드의 수행권한이 있는 것이다.
    • add 와 subtract 에 대한 별도의 락이 존재하지 않는다.

스태틱 메서드에서의 synchronized 블록

public static MyStaticCounter{

  private static int count = 0;

  public static synchronized void add(int value){
    count += value;
  }

  public static synchronized void subtract(int value){
    count -= value;
  }
}
  • 한 번에 JVM 내에 존재하는 하나의 스레드만 메서드에 접근이 가능하다.
    • 인스턴스 내부에 있던 synchronized 와 원리는 같다. 클래스를 monitor 로 사용하는데, static 키워드 덕에 JVM에 하나의 클래스만 존재하기 때문이다.

인스턴스 메서드 내부에서의 synchronized 블록

public class MyClass {

  public synchronized void log1(String msg1, String msg2){
     log.writeln(msg1);
     log.writeln(msg2);
  }


  public void log2(String msg1, String msg2){
     synchronized(this){
        log.writeln(msg1);
        log.writeln(msg2);
     }
  }
}
  • synchronized(this) { ... } 에서 쓰이는 this 가 monitor 오브젝트로 사용된다.
  • 그래서 위의 synchronized 메서드와 synchronized 블록문은 절대 같은 타이밍에 동시에 실행될 수 없다.
  • 만일 두번째 synchronized 블록문에서 (this) 가 아니라 다른 오브젝트였다면, 각각 동시에 실행될 수 있는 코드가 된다.

람다에서의 synchronized 블록

import java.util.function.Consumer;

public class SynchronizedExample {

  public static void main(String[] args) {

    Consumer<String> func = (String param) -> {

      synchronized(SynchronizedExample.class) {

        System.out.println(
            Thread.currentThread().getName() +
                    " step 1: " + param);

        try {
          Thread.sleep( (long) (Math.random() * 1000));
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println(
            Thread.currentThread().getName() +
                    " step 2: " + param);
      }

    };


    Thread thread1 = new Thread(() -> {
        func.accept("Parameter");
    }, "Thread 1");

    Thread thread2 = new Thread(() -> {
        func.accept("Parameter");
    }, "Thread 2");

    thread1.start();
    thread2.start();
  }
}
  • 람다에서도 가능하다.

synchronized 블록문을 사용하며 주의할 점

monitor 객체가 될 대상에 String Integer 등 불변 객체를 주지 말자.

JVM 내부에서 String, Integer 등 특정 객체들에 대해 최적화를 해주기 때문에 이러한 Primitive Wrapper 객체들을 monitor 객체로 사용하는 경우엔 의도치 않은 결과를 초래할 수 있다.

this나 new Object()를 사용하는 것은 문제 없다.

읽기, 쓰기를 따로 구분해서 구현하고 싶다면

만일 한번에 2개의 스레드가 공유된 값을 읽기만 하고 업데이트 하지 않을 것이라면? 이는 thread-safe 하다고 볼 수 있다. 이 경우에는 synchronized 만으로 구현할 수 없는데 이 때는 ReadWriteLock 클래스를 이용하면 된다.

2개 이상의 스레드가 블록문에 들어오도록 구현하고 싶다면

N 개의 스레드가 synchronized 블록에 들어오게 하고 싶다면? Semaphore 를 이용해서 구현할 수 있다.

들어오는 스레드의 순서를 정하고 싶다면

synchronized 블록에 들어오는 스레드의 순서는 따로 정해져있지 않다. Fairness 를 구현해서 정해줄 수 있다.

하나의 스레드만 쓰기 역할을 하고 다른 스레드는 읽게 하고 싶다면?

volatile variable 을 잘 살펴보면 된다.

synchronized 블록의 오버헤드

synchronized 블록문에 입장하고 퇴장하는데 아주 약간의 오버헤드가 있는 편이다. 그러므로 꼭 필요한 부분만 synchronized로 묶는 것이 중요하다.

synchronized 블록에 재입장하는 것도 허용된다.

public class MyClass {
  List<String> elements = new ArrayList<String>();

  public void count() {
    if(elements.size() == 0) {
        return 0;
    }
    synchronized(this) {
       elements.remove();
       return 1 + count();  
    }
  }
}

위 코드처럼 하나의 스레드가 this 인스턴스를 monitor 오브젝트로 잡아놓고, count() 를 호출하여 다시 해당 인스턴스에 synchronized 블록에 재입장할 수 있다.

또 다른 오브젝트의 synchronized 블록에 입장하는 것도 가능한데, 잘못된 디자인을 할 경우 nested monitor lockout을 유발할 수 있기 때문에 주의해야 한다.

레퍼런스

  • 나무위키: 경쟁상태
  • 젠코브 자바 튜토리얼
  • jls 17.1
반응형
저작자표시 (새창열림)

'Java > 자바 잡지식' 카테고리의 다른 글

자바 EE 란?  (0) 2022.04.20
클린 아키텍처 (by Robert C. Martin) 번역  (0) 2022.04.14
Test Double 이란?  (0) 2022.04.13
Gradle 에서 Plugins 의 역할은 무엇일까?  (0) 2022.04.03
자바에서 주석 다는 방법: Javadoc 이란 무엇일까? (Feat. 위키피디아)  (0) 2022.03.30
    'Java/자바 잡지식' 카테고리의 다른 글
    • 클린 아키텍처 (by Robert C. Martin) 번역
    • Test Double 이란?
    • Gradle 에서 Plugins 의 역할은 무엇일까?
    • 자바에서 주석 다는 방법: Javadoc 이란 무엇일까? (Feat. 위키피디아)
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바