StringBuffer
는 동기화 메커니즘을 사용해 여러 스레드가 동시에 접근해도 안전함.StringBuilder
는 동기화가 없어서 Thread-Safe하지 않음.count++
는 단순한 증가 연산처럼 보이지만, 내부적으로는 read-modify-write
과정이 포함되어 있습니다. 이 과정 중 다른 스레드가 접근하면 충돌이 발생하여 정확한 결과를 보장하지 못하는 문제가 발생합니다.원자성이란 특정 작업이 더 이상 쪼갤 수 없는 하나의 단위로 실행되는 성질을 의미합니다.
즉, 작업이 모두 실행되거나 전혀 실행되지 않는 상태(실패 시 아무 변화도 없는 상태로 복구되는 것)를 보장하는 것을 말합니다.
동시성 프로그래밍(여러 작업을 동시에 처리할 수 있도록 설계된 프로그램을 개발하는 방식)에서는 CPU와 RAM의 중간에 위치하는 CPU Cache Memory와 병렬성이라는 특징때문에 다수의 스레드가 공유 자원에 접근할 때 두 가지 문제가 발생할 수 있다.
자바에서는 이런 문제를 해결하기 위해 여러 가지 도구를 제공합니다:
volatile
:
synchronized
:
Atomic
클래스:
AtomicInteger
, AtomicLong
등)를 사용합니다.Lock 인터페이스:
ReentrantLock
, ReadWriteLock
등)을 활용해 정교한 락 관리가 가능합니다.Lock 인터페이스란?
Lock
인터페이스는 자바에서 스레드 간의 자원 접근을 제어하기 위해 사용됩니다. 기존의synchronized
키워드와 비교해, 다음과 같은 특징을 가집니다:
- 락 획득과 해제를 명시적으로 처리 (
lock()
과unlock()
메서드 사용).- 락 대기 시간 설정 가능 (
tryLock()
메서드).- 조건 변수를 사용하여 보다 정교한 스레드 간 통신 가능 (
Condition
객체).- 성능 최적화 가능.
volatile
키워드synchronized
키워드역할:
"임계영역(Critical Section)" : 멀티스레드 프로그래밍에서 공유 자원(Shared Resource)에 대해 동시에 여러 스레드가 접근하면 안 되는 코드 블록
사용 방법:
public synchronized void increment() {
count++;
}
public void increment() {
synchronized (this) {
count++;
}
}
synchronized
의 문제점
synchronized
키워드는 동시성 제어를 위해 락을 사용하는데, 락은 다음과 같은 이유로 성능 저하를 유발합니다:
- Thread Contention(스레드 경합):
- 여러 스레드가 동일한 락을 얻기 위해 경쟁합니다.
- 대기 시간이 발생하며, context switching 비용도 추가됩니다.
- 락 대기 정책:
- 락을 획득하지 못한 스레드는 대기 상태로 전환됩니다.
- 이는 운영 체제의 스케줄링 정책에 따라 다르며, 추가적인 오버헤드가 있습니다.
- 모니터와 대기 큐:
- synchronized는 내부적으로 모니터(Monitor)라는 객체를 사용하며, 락 대기 스레드가 모니터의 대기 큐에서 대기합니다.
- 락을 해제하고 스레드가 깨어나는 과정에서 시간이 소요됩니다.
1. 성능 저하
ReentrantLock
은 락을 세밀하게 제어할 수 있도록 도와주며, 필요에 따라 tryLock()
을 활용해 타임아웃을 설정하거나, 공정 모드를 활성화해 성능을 최적화할 수 있습니다.AtomicInteger
, AtomicReference
와 같은 원자 클래스(Atomic Classes)를 활용해 락 없이도 스레드 안전하게 데이터를 수정할 수 있습니다.ReentrantLock
의 공정 모드를 활성화하면 락 요청 순서에 따라 락을 배정해 특정 스레드가 계속 대기하지 않도록 합니다. ReentrantLock fairLock = new ReentrantLock(true); // 공정성 활성화
ConcurrentHashMap
이나 ConcurrentLinkedQueue
와 같은 락 없는 스레드 안전 데이터 구조를 활용해 공정성과 성능을 동시에 확보할 수 있습니다.synchronized
는 어떻게 구현되어 있나요?synchronized
의 작동 방식Java에서 synchronized는 다음 과정을 통해 동작하며, 내부적으로는 JVM에서 이를 관리합니다.
(1) 모니터 락(Monitor Lock)
synchronized
는 해당 객체의 모니터 락(모니터 내부의 락) 을 점유함으로써 동기화된 코드 블록에 대한 접근을 제어합니다.(2) 바이트코드와 모니터 엔트리/출구
synchronized
키워드를 만나면 해당 부분을 바이트코드의 monitorenter
와 monitorexit
명령으로 변환합니다.monitorenter
: 락을 획득합니다.monitorexit
: 락을 해제합니다.(3) 구현의 주요 원리
객체 헤더(Object Header)는 JVM이 객체를 관리하는 데 필요한 정보를 저장하는 메타데이터입니다. 일반적으로 다음과 같이 구성됩니다.
Mark Word: 객체의 상태(락, 해시 코드, GC 정보 등)를 저장. synchronized의 락 상태 관리에 사용됩니다.
클래스 포인터: 객체가 어떤 클래스의 인스턴스인지 가리키는 참조.
Array Length (배열 객체에만 해당): 배열의 길이를 저장.
count++
는 atomic하지 않음 (read, modify, write로 분리).Atomic 타입은 스레드 안전성(thread-safety)을 보장하면서 연산이 "atomic"하게 이루어지도록 설계된 데이터 타입입니다. Java에서는 java.util.concurrent.atomic
패키지에 포함된 클래스들이 대표적인 atomic 타입에 해당합니다.
AtomicInteger
: 정수형 데이터를 atomic하게 다루는 클래스.AtomicLong
: Long 타입 데이터를 atomic하게 다루는 클래스.AtomicBoolean
: Boolean 타입 데이터를 atomic하게 다루는 클래스.AtomicReference<V>
: 참조형 데이터를 atomic하게 다루는 클래스.