본문 바로가기

연재작/WEB - BE

JVM Runtime Data Area, Singleton과 Race Condition

 

 

1. Runtime Data Area

JVM은 크게 Class Loader, Runtime Data Area, JVM Engine으로 구성되어 있고

그 중 Runtime Data Area는 JVM의 메모리 영역으로,

우리가 흔히 알고 있는 Thread, Heap, Static영역(혹은 method, class영역)으로 구분되어 있다.

  • Thread 영역은 지역 변수, 파라미터, 리턴 값을 담당, 메서드 호출 시 사용하는 영역이다.
  • Heap 영역은 객체의 저장을 담당한다.
    • java는 class 기반의 객체 지향 프로그래밍 언어이기에, 많은 객체가 생성되고 소멸된다.
    • 객체의 소멸, 데이터 이관을 관리하기 위해서 Garbage Collection을 시행하는데, Heap영역이 그 대상이다.
  • Static 영역은 전역변수, Static 변수, Final Class, Class 의 필드와 메서드 정보 등
    프로그램의 시작부터 끝까지 메모리에 상주한다.
    • 그렇기 때문에 static, method(객체화 되기 전 class의 메서드), class 영역으로 다른 방식으로 부른다.

 

2. Singleton pattern - 하나의 진입점

싱글턴 패턴은 정말 많이 설명한것 같지만, 다시 한 번 더 써보겠다.

  1. 클래스 내에 단 하나의 인스턴스만 존재 - 필요 시 인스턴스를 생성하거나 기존의 인스턴스 반환
  2. 글로벌 접근 - static 선언을 통해 어디서든 접근 가능
  3. 메모리 효율성 - 불필요한 객체 생성을 막는다.

요약한 싱글턴 패턴의 골자는 하나의 클래스에 대해 오직 하나의 인스턴스만 생성되도록 보장하는 것이다.

 

singleton의 코드를 다시 한번 보여주자면 다음과 같다.

public class Singleton {

    private Singleton() {} // 생성자 캡슐화
    
    private static Singleton uniqueInstance; // 싱글턴 인스턴스의 static 선언
    
    public static Singleton getInstance() { 
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }  // 정적 팩토리 메서드 + 인스턴스 재사용
}

 

이렇게 선언 할 경우, JVM 내의 Class Loader 커스텀을 따로 하지 않았다는 가정 하에

일반적으로 한 개의 Singleton 클래스가 클래스 로더에 올라가게 되고,

싱글 스레드 상황이었다면 유일한 singleton 객체가 정상적으로 Heap 영역에 적재된다.
(Class Loader가 두 개 이상이면 서로 다른 클래스와  생성됐을 것.)

  • private static Singleton uniqueInstance와 같은 정적 변수는 Static 영역에 참조값만 저장
  • 실제 객체 인스턴스는 Heap 영역에 적재

* 참고로, 커스텀 클래스로더가 사용되는 경우?

더보기

어플리케이션 서버에서 각 어플리케이션마다 독립적인 클래스로더를 사용해 충돌을 방지할 때 사용된다.

 

그런데 문제는 getInstance의 호출이 멀티 스레드에서 이루어 졌을 때 발생한다.

 

 

 

3. Race Condition & Concurrency Issue

단일 진입점에 여러개의 호출,

즉 다중 스레드에서 getInstance라는 메서드를 호출 했을 때 접근 순서에 따라 결과가 달라질 수 있다.

여러 개의 스레드를 사용한다고 가정했을 때, 스레드별로 영역을 지정해주지 않아 접근제어를 해주지 않는다면

단일 진입점인 getInstance 함수의 호출을 두 스레드에서 동시에 진입하는 것이 가능하다.

그런데 만약 두 개가 접근한 상태에서 uniqueInstance가 null인 상태라면???

그렇다면 두 스레드 모두 new Singleton() 을 통해 새로운 객체를 생성해

하나의 클래스로더 내에 두 개의 싱글턴 객체가 존재하게 된다.

 

이처럼, 여러 스레드가 동시에 접근할 때 접근 순서에 따라 결과가 달라질 수 있는 상황을 Race Condition 이라고 한다.

그리고 이와 같이 동시에 접근 했을 때 발생할 수 있는 문제를 포괄적으로 Concurrency Issue(동시성 문제)라 한다.

 

그렇다면, 이와 같은 Race Condition을 해결할 수 있는 방법에는 뭐가 있을까?

 

 

4. Blocking

단위별로 Blocking을 사용하면 된다.

즉, 조금이라도 먼저 접근한 스레드가 해당 함수를 수행하는 동안 다른 스레드가 접근하지 못하도록 기다리게 하는 것이다.

 

4.1. Synchronized 키워드 사용

함수 단위의 blocking이다.

public class Singleton {

    private Singleton() {}
    
    private static Singleton uniqueInstance; 
    
    public static synchronized Singleton getInstance() { 
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }  // 정적 팩토리 메서드 + 인스턴스 재사용 + synchronized 추가
}

단, 메서드 전체를 blocking할 경우 성능상의 문제가 있을 수 있다.

 

4.2. volatile - 가시성 보장

가시성이란, 동기화 문제에서 한 스레드 값(CPU 캐시의 값)이 다른 스레드에서 볼 수 있는지의 여부이다.

volatile 키워드를 통해 이를 확인할 수 있고, 여기에 write 할 때 block을 하도록 해서 다음과 같이 하는 것이 가능하다.

public class Singleton {

    private Singleton() {} 
    
    private volatile static Singleton uniqueInstance; // 변수에 대한 가시성 제공
    
    public static Singleton getInstance() { 
    	if (uniqueInstance == null) {
        	synchronized (Singleton.class) {
            // null 일 때 쓰기가 들어가므로, 이때 synchronized 를 통해 blocking
            	if (uniqueInstance == null) {
                	uniqueInstance = new Singleton();
                    //blocking 후 쓰기
                }
            }
        }
        return uniqueInstance;
    }  // 정적 팩토리 메서드 + 인스턴스 재사용
}

이는 사실상 동기화의 체킹을 두 번 진행하는 것이다. 이와 같은 방식을 Double-Checked Locking 방식이라고 한다.

첫 번째 체크에서 성능을 높이고, 두 번째 체크에서 동시성 문제를 방지하는 셈이다.

 

4.3. Lazy Holder

Lazy Holder는 해당 변수를 사용할 때 Blocking을 해 완벽히 생성한 것을 보장한다.

사용시에 문제가 없는 것을 보장하는 방식이다.

public class Singleton {

    private Singleton() {}
    
    private static class Holder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
    	return Holder.INSTANCE;
    }
}

내부 정적 클래스인 Holder는 해당 클래스가 처음 참조될 때 초기화되기에,

동기화가 따로 없어도 thread-safe하게 객체를 생성하는 것이 가능하다.

 

5. 동시성이 내장된 클래스

  • AtomicInteger를 비롯한 Atomic Reference들
  • ConcurrentHashMap
  • SynchronizedMap

이와 같은 것들은 따로 설정하지 않아도 구현상의 동시성을 미리 제공한다.

 

 

 

 

 

https://www.baeldung.com/java-synchronizedmap-vs-concurrenthashmap

 

https://aaronryu.github.io/2019/03/02/singleton-pattern/

 

6. 싱글턴 패턴

디자인 패턴은 무조건 아래 두 글을 선행해야합니다. 짧으니 간단히 읽고 오시면 이해가 쉽습니다. 1. 디자인 패턴에 앞서 2. 디자인 패턴의 제 1, 2 원칙 설명에 사용할 코드는 Java-like Pseudo Code 입

aaronryu.github.io

https://aaronryu.github.io/2021/03/14/unboxing-when-use-equal-operator-on-wrapper-class/

 

Wrapper Class Caching: Integer(Wrapper Class) == 사용시 이슈

얼마전부터 서버에서 Integer 객체를 == (항등 연산자)를 사용한 코드때문에 간간히 에러 로그가 남는것을 확인했습니다. 신기한건 해당 API 가 매우 자주사용되는데, 간헐적으로 발생한다는 것이

aaronryu.github.io

https://roadofdevelopment.tistory.com/entry/%EB%A9%80%ED%8B%B0%EC%93%B0%EB%A0%88%EB%93%9C%ED%99%98%EA%B2%BD%EB%8F%99%EC%8B%9C%EC%84%B1%EC%A0%9C%EC%96%B4-%08-AtomicInteger-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0

 

멀티쓰레드환경,동시성제어 | AtomicInteger 활용하기

멀티쓰레드 환경에서 공유 리소스에 대한 동시성 제어가 필요한 예제를 알아본다. sychronized 등을 통한 동시성제어가 아니라 AtomicXXX 클래스(AtomicIntger)를 통해 동시성제어를 구현해본다. 1.멀티쓰

roadofdevelopment.tistory.com