여러 작업을 한 번에 다루는 개념이지만, 실제 의미와 구현 방식에서 차이가 있다.

작업이 동시에 발생하는 것처럼 보이기 위해 번갈아 가며서 작업을 수행하는 것
실제로 독립적으로 동시에 작업을 수행하는 것
멀티 스레드 환경인 자바에서는 이 동시성과 병렬성에서 어떤 문제가 있고 어떻게 해결할까?
멀티 스레드에서는 공유 자원에 동시에 여러 스레드가 접근할 때, 공유 자원이 변경되어 다른 스레드 작업에 영향을 미쳐 올바른 결과를 반환하지 못하게 된다. 이를 동시성 문제라고 한다.
자바에서는 이런 동시성 문제를 해결해주기 위해 여러가지 방법을 제공해주고 있습니다.
자바에서 제공해주는 concurrent 패키지의 일부분입니다. atomic 패키지도있고 Thread Safe하면 많이 알고 있는 Concurrentmap도 여기에 구현이 되어 있습니다.
스레드 안전(thread 安全, 영어: thread safety)은 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다. 보다 엄밀하게는 하나의 함수가 한 스레드로부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것으로 정의한다.
(1) Re-entrancy
어떤 함수가 한 스레드에 의해 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하더라도 그 결과가 각각에게 올바로 주어져야 한다.
(2) Thread-local storage
공유 자원의 사용을 최대한 줄여 각각의 스레드에서만 접근 가능한 저장소들을 사용함으로써 동시 접근을 막는다.
이 방식은 동기화 방법과 관련되어 있고, 또한 공유상태를 피할 수 없을 때 사용하는 방식이다.
(3) Mutual exclusion
공유 자원을 꼭 사용해야 할 경우 해당 자원의 접근을 세마포어 등의 락으로 통제한다.
(4) Atomic operations
공유 자원에 접근할 때 원자 연산을 이용하거나 '원자적'으로 정의된 접근 방법을 사용함으로써 상호 배제를 구현할 수 있다.
동시성(멀티스레드/멀티프로세스) 프로그래밍 환경에서 자주 등장하는 문제 중 대표적인 것이 가시성(Visibility) 문제와 원자성(Atomicity) 문제입니다. 이 두 가지는 제대로 된 동기화 메커니즘을 사용하지 않을 경우 프로그램이 예측할 수 없는 동작을 보이게 하는 원인이 됩니다.
1 → 2로 업데이트했다고 해서, 그 값이 곧바로 메인 메모리에 쓰이지 않을 수 있습니다(캐시에만 존재).volatile 키워드나 synchronized 블록 등을 통해 ‘메모리 가시성’을 보장하도록 합니다. volatile은 해당 변수에 대한 읽기/쓰기가 항상 메인 메모리에서 이루어지도록 하여, 다른 스레드가 즉시 바뀐 값을 볼 수 있도록 만듭니다.synchronized 블록에서는 블록 진입 시점과 종료 시점에 관련된 메모리 배리어(Memory Barrier) 를 설정하여 값을 제대로 밀어 넣고 꺼내게 만듭니다.class VisibilityExample {
private static boolean ready = false; // 가시성 문제가 발생할 수 있는 공유 변수
public static void main(String[] args) {
new Thread(() -> {
while (!ready) {
// ready가 false인 것으로 계속 인식해 멈추지 않을 수 있음
}
System.out.println("Thread1 sees ready == true");
}).start();
new Thread(() -> {
// 잠깐 대기 후 ready를 true로 설정
try { Thread.sleep(100); } catch (InterruptedException e) {}
ready = true;
System.out.println("Thread2 sets ready == true");
}).start();
}
}
ready 변수에 대한 가시성이 제대로 보장되지 않으면, 첫 번째 스레드는 ready의 변경을 인식하지 못해 무한 루프에 빠질 수도 있습니다.ready 변수에 volatile을 선언하거나 synchronized로 감싸면 이 문제를 해결할 수 있습니다.단순히 정수 하나를 갱신하거나 하나의 값을 읽는 연산이라 하더라도, 실제로는 CPU 입장에서 load -> update -> store 같은 여러 단계로 나누어집니다.
예를 들어 i++를 수행할 때,
이 과정 중 다른 스레드가 중간 단계에 끼어들어 값을 변경하거나 읽을 수 있습니다.
이를 방치하면 원하는 결과보다 적게(또는 더 많이) 증가되는 등 예상치 못한 결과가 나올 수 있습니다.
class AtomicityExample {
private int count = 0;
public void increment() {
// count++ 는 실제로 여러 단계로 나뉘어 수행됨
count++;
}
}
increment()를 동시에 호출할 경우, 원자성이 깨져서 최종 count 값이 기대했던 만큼 증가하지 않을 수 있습니다.synchronized 키워드나 AtomicInteger 등의 동시성 유틸을 사용해 원자적 연산을 보장해야 합니다.volatile 키워드를 사용해 변수의 읽기·쓰기 연산을 항상 메인 메모리와 동기화 synchronized 블록(또는 ReentrantLock)으로 락을 사용해 진입 시점과 종료 시점에 메모리 배리어(Memory Barrier) 발생 Atomic* 클래스, Concurrent 패키지) 사용i++ 같은 간단해 보이는 연산도 실제로는 “읽기 → 증가 → 쓰기” 3단계로 분할됨.synchronized 키워드나 Lock(예: ReentrantLock)으로 임계영역(Critical Section)을 보호AtomicInteger, AtomicReference 등 원자적 연산을 제공하는 클래스를 이용해 락 없이 안전한 연산 수행tryLock(long time, TimeUnit unit))으로 일정 시간 기다린 후 해제 시도ArrayList, HashMap 등을 여러 스레드에서 동시에 수정하면 Race Condition이 쉽게 발생Collections.synchronizedList(...)등을 통해 동기화 래퍼를 씌울 수 있지만, 성능 저하가 심하고 세밀한 동시 접근 제어는 어렵습니다.java.util.concurrent 패키지에서 지원하는 고성능 동시성 컬렉션 ConcurrentHashMap, ConcurrentLinkedQueue, CopyOnWriteArrayList 등Executor, ExecutorService, ThreadPoolExecutor 등을 통해 스레드 풀을 효율적으로 관리.shutdown vs shutdownNow) 예상치 못한 결과를 유발Future 인터페이스CompletableFuture는 더 풍부한 콜백 연계(thenApply, thenCombine 등)를 지원.parallel())하여 멀티코어 활용ForkJoinPool 사용Flow API (Java 9+) 기반, 확장 라이브러리 사용 시 주의점(백프레셔, 구독 해지 등)volatile/synchronized/Atomic 클래스 등으로 적절히 동기화해야 함.ConcurrentHashMap, CopyOnWriteArrayList 등 안전하면서도 고성능을 낼 수 있는 컬렉션 활용.ExecutorService, ThreadPoolExecutor를 적절히 구성, 모니터링 및 튜닝이 필요.CompletableFuture, 병렬 스트림, 리액티브 라이브러리 등을 통해 복잡한 동시성 제어 로직을 단순화 가능.자바의 동시성은 단순히
synchronized키워드나 스레드 생성으로 해결되지 않으며,
자바 메모리 모델에 대한 이해와 동시성(멀티스레드) 설계 원칙이 매우 중요합니다.
올바른 동기화, 적절한 락 사용, 혹은 락 프리(lock-free) 알고리즘 적용 등을 통해
안전하고 효율적인 멀티스레드 프로그램을 작성하는 것이 핵심입니다.
자바 volatile 키워드는 멀티스레드 환경에서 공유 변수(필드) 접근 시 가시성(Visibility) 문제를 해결하기 위해 사용하는 키워드입니다.
volatile의 주요 특징가시성(Visibility) 보장
volatile로 선언된 변수에 대한 읽기·쓰기 연산은 항상 메인 메모리와 동기화됩니다. volatile 변수를 갱신하면, 다른 스레드에서 즉시 바뀐 값을 볼 수 있습니다(캐시에만 남아있지 않음). volatile은 읽기/쓰기에 메모리 배리어(Memory Fence) 를 적용하여, 원자성(Atomicity)은 보장하지 않음
volatile 변수라 하더라도, 이를 활용한 복합 연산(i++, count += x 등)은 여전히 원자적이지 않습니다. volatile int count; count++; 같은 코드는 내부적으로 여러 단계(읽기→증가→쓰기)로 나뉘기 때문에,AtomicInteger 같은 원자성 보장 클래스나 synchronized 블록을 사용해야 합니다.스레드 간 재정렬(Reordering) 방지
volatile 변수에 대한 접근 시점에는 재정렬이 제한되어, 다른 스레드가 예상하지 못한 순서로 실행 결과를 보게 될 위험을 줄여줍니다.volatile 변수| 구분 | 일반 변수 | volatile 변수 |
|---|---|---|
| 가시성 | ||
| 재정렬 방지 | ||
| 원자성 |
boolean isRunning)처럼 읽기·쓰기가 모두 단일 단계인 변수라면 volatile만으로도 충분히 동기화를 보장할 수 있습니다. volatile만으로는 부족하고, 다른 동기화 메커니즘(락, Atomic 클래스 등)을 사용해야 합니다.상태 플래그 제어
public class VolatileExample {
private volatile boolean running = true; // 플래그
public void start() {
new Thread(() -> {
while (running) {
// running이 false가 되는 즉시 인식 가능
}
System.out.println("Thread stopped.");
}).start();
}
public void stop() {
running = false; // 다른 스레드에서 즉시 인식
}
}
volatile로 선언된 running은 가시성이 보장되어, stop()에서 running = false로 바꾸면 start() 메서드 내의 스레드가 즉시 변경 사항을 인식하게 됩니다. 단순 카운터
public class VolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // 이 연산은 원자적이지 않음
}
}
이 코드는 volatile을 사용했어도, 여러 스레드가 동시에 increment()를 호출하면 Race Condition이 발생할 수 있습니다.
만약 “정확한 카운팅”이 필요하다면 아래와 같이 원자적 클래스를 쓰는 편이 낫습니다.
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 원자적 연산 보장
}
volatile 키워드는 동시성(Concurrency) 프로그래밍에서 가시성 문제와 부분적으로 재정렬 문제를 해결하기 위한 기초적인 도구입니다. volatile만으로 보장되지 않으므로, 필요한 경우에는 락(예: synchronized) 또는 Atomic 클래스 같은 다른 동기화 기법을 적용해야 합니다. volatile은 효율적이고 적절한 선택입니다.자바에서 synchronized 키워드는 임계영역(Critical Section)을 설정하고, 해당 블록(또는 메서드)을 단일 스레드만 접근 가능하도록 락(Lock)을 거는 동기화 수단입니다. 이를 통해 여러 스레드가 동시에 공유 자원(객체 변수, 정적 변수 등)에 접근할 때 발생할 수 있는 원자성(Atomicity), 가시성(Visibility), Race Condition 문제 등을 예방합니다. 아래에서 좀 더 자세히 살펴보겠습니다.
synchronized 키워드란?메서드 선언부에 사용
public synchronized void increment() {
// 임계영역 (이 메서드는 한 번에 오직 1개의 스레드만 진입 가능)
}
static)에 synchronized를 붙일 경우, 해당 클래스의 Class 객체가 락으로 사용됩니다.블록에 사용
public void increment() {
synchronized (lockObject) {
// 임계영역
}
}
lockObject)를 락으로 사용하여 그 블록 안에만 동기화를 적용.lockObject를 사용하면, 해당 락을 기준으로 진입이 상호 배타적으로 제한됩니다.synchronized 구문은 모니터(Monitor) 라 불리는 락(잠금) 메커니즘을 사용합니다.monitorenter와 monitorexit 바이트코드 명령이 추가되어, 해당 객체 모니터를 획득(acquire)하고 해제(release)합니다.synchronized는 재진입 가능(Reentrant) 락입니다. lockObject의 모니터를 획득한 상태에서, 동일 스레드가 같은 객체 락을 다시 획득하려 해도 계속 진행됩니다(락 횟수 카운트가 1씩 증가). synchronized의 문제점synchronized 블록/메서드로 감싸면, 해당 락을 획득하기 위해 스레드는 대기해야 하므로 블로킹(Blocking)이 발생합니다. synchronized는 지정된 객체 모니터(또는 클래스 모니터)에 대해 배타적으로 동작하므로,synchronized만으로는 락 순서, 타임아웃, 공정성(Fairness) 등을 세밀하게 제어하기 어렵습니다. ReentrantLock, ReadWriteLock 같은 java.util.concurrent.locks 패키지의 락을 사용하는 것이 유연합니다.synchronized는 기본적으로 임계영역 보호가 목적입니다. wait() / notify() 같은 Object 모니터 메서드를 사용해야 하며,synchronized의 내부 구현 방식자바 소스 코드를 JVM 바이트코드 레벨에서 보면, synchronized 블록에는 monitorenter, monitorexit 명령이 추가됩니다.
monitorenter
monitorexit
자바 11 이후로는 Biased Locking이 기본적으로 비활성화되거나 제거될 움직임도 있으나,
전체적인 개념은 “처음엔 가벼운 방식으로 락을 잡고, 경쟁이 심해지면 무겁게 전환”하는 다단계 구조를 취한다는 점입니다.
정의
synchronized는 자바에서 임계영역을 설정하고 동시에 접근하는 스레드를 순차화하기 위한 키워드.장점
문제점
구현 원리
monitorenter, monitorexit 명령 추가. 결론적으로,
synchronized는 자바에서 가장 기본적이고 간편한 동기화 수단입니다.
다만 빈번한 락 충돌이나 복잡한 동기화 시나리오(락 타임아웃, 다중 조건 대기 등)에서는
Lock인터페이스(ReentrantLock, ReadWriteLock 등)나 동시성 라이브러리를 사용하는 것이 더 유연하고 나을 수 있습니다.
예를 들어, “i를 1 증가시키는 연산(i++)” 같은 단순해 보이는 작업도 실제로는
1) i값 읽기
2) i값을 1 증가
3) 증가된 i값 쓰기
라는 세 단계로 나뉩니다.
이 세 단계가 원자적으로(Atomic) 보장되지 않으면, 여러 스레드가 동시에 해당 연산을 호출했을 때 예상치 못한 결과가 나올 수 있습니다.
불가분(Indivisible)
중단되거나 간섭받지 않음
Race Condition 방지
자바에서 java.util.concurrent.atomic 패키지에 있는 Atomic 클래스들을 가리키는 경우가 많습니다. 예컨대:
AtomicInteger AtomicLong AtomicBoolean AtomicReference<T>이들은 내부적으로 Compare-And-Set(CAS) 같은 CPU의 원자적(Atomic) 명령을 사용하여, 락(lock) 없이도 스레드 안전하게(중간단계 없이) 값을 읽거나 수정할 수 있도록 지원합니다.
AtomicInteger 예시private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
// 원자적으로 counter 값을 1 증가
counter.incrementAndGet();
}
incrementAndGet()는 내부적으로 “기존 값 읽기 → 1 증가 → 저장” 과정을 하나의 불가분 연산으로 처리합니다. int count; count++; 같은 연산은 스레드가 여러 개일 때 순서 문제로 값이 제대로 증가되지 않을 수 있음. (원자성 X) AtomicInteger 등 Atomic 타입은 “복합 연산도 안전하게” 보장 synchronized 또는 Lock보다 빠르게 동작원자성(Atomicity)
락 오버헤드 줄이기
synchronized 블록이나 ReentrantLock 등을 통해 원자성을 확보함(임계영역화) 가시성(Visibility)도 함께 보장
AtomicInteger, AtomicLong 등, 내부적으로 CAS를 사용해 원자적 연산을 제공하는 클래스. 결국 동시성 프로그래밍에서 “원자성”은 매우 중요한 개념이며, Atomic 타입을 적절히 활용하면 락을 최소화하면서도 안전하게 공유 데이터를 처리할 수 있습니다.