티스토리 뷰

Study/CS 스터디 - Java

동시성

JJIINDOL 2025. 4. 3. 22:21

📖 동시성과 병렬성

동시성(논리적 개념)

하나의 시스템이 여러 작업을 동시에 처리하는 것처럼 보이게 하는 것, 실질적으로는 한 번에 하나의 작업만을 처리한다.

 

병렬성(물리적 개념)

여러 작업을 실제로 동시에 처리하는 것. 작업들은 각각이 독립적으로 실행되며 서로 영향을 주지 않는다.

 

📖 동시성 이슈 해결

synchronized 키워드 사용

synchronized 키워드는 자바에서 가장 기본적인 동기화 방식이다. 여러 스레드가 동시에 공유 자원에 접근하는 경우, 해당 메서드나 코드 블록에 synchronized를 사용하면 한 번에 하나의 스레드만 접근할 수 있도록 락을 건다.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

 

 

volatile 키워드 사용

volatile 키워드는 변수의 값을 캐시하지 않고 항상 메인 메모리에서 읽도록 보장한다. 이를 통해 스레드 간의 가시성 문제를 해결할 수 있지만, count++ 같은 복합 연산은 원자성이 보장되지 않아 synchronized나 Atomic 클래스가 필요할 수 있다.

public class Flag {
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // 실행 중
        }
    }
}

 

 

동시성 컬렉션 사용 (java.util.concurrent)

java.util.concurrent 패키지의 동시성 컬렉션은 멀티스레드 환경에서도 안전하게 사용할 수 있도록 설계된 자료구조이다. 예를 들어, ConcurrentHashMap은 내부적으로 세그먼트를 나누어 부분 락을 사용하기 때문에 성능도 좋고 안정성도 확보된다.

import java.util.concurrent.ConcurrentHashMap;

public class Example {
    private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

    public void putValue(String key, String value) {
        map.put(key, value);
    }

    public String getValue(String key) {
        return map.get(key);
    }
}

 

 

Atomic 클래스 사용 (java.util.concurrent.atomic)

java.util.concurrent.atomic 패키지의 Atomic 클래스는 락 없이도 원자 연산을 보장한다. 내부적으로는 CAS(Compare-And-Swap) 기법을 사용하여 경합을 줄이고 성능을 높인다. AtomicInteger를 사용하면 count++ 연산을 안전하게 처리할 수 있다.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

 

* CAS: 멀티스레드 환경에서 락 없이 값을 변경하기 위한 원자적 연산

 

Lock 객체 사용 (ReentrantLock)

ReentrantLock은 synchronized보다 유연한 락 제어가 가능한 클래스이다. 락 획득 시도를 할 수 있고, 타임아웃을 줄 수도 있으며, 인터럽트가 가능한 등 제어권이 많다. 단, 락을 직접 해제해야 하기 때문에 finally 블록에서 반드시 unlock()을 호출해야 한다.

import java.util.concurrent.locks.ReentrantLock;

public class LockCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

 

📖 Synchronized

 

💡 synchronized 키워드란?

Synchronized 우리 말로 '동기화된'이라는 뜻이다. 자바에서의 Synchronized는 말 그대로 공유될 가능성이 존재하는 자원에 대해 동기화를 제어할 수 있게 해주는 키워드이다. 자바에서 해당 키워드를 붙일 경우 해당 자원에 대해 오직 하나의 스레드만 접근할 수 있다. 어떠한 스레드가 해당 자원에 접근 중이고 다른 스레드가 같은 자원에 접근하려고 할 경우 접근을 대기시킨다.

//메서드에 사용
public synchrozied void incrementCount() {
	this.count++;
}


//코드 블록에 사용
public void incrementCount() {
	synchronized(this) {
    	count++;
    }
}

 

 

💡 synchronized의 문제점

 

✔️ 성능 저하

synchronized는 한 번에 하나의 스레드만 임계 영역에 진입할 수 있도록 하기 때문에, 멀티스레드 환경에서는 병렬 처리의 이점이 줄어들고 처리 속도가 느려질 수 있다.

 

✔️ 데드락

synchronized 블록 안에서 다른 락을 가진 메서드나 객체에 접근할 경우, 두 스레드가 서로 상대방의 락이 풀리기를 기다리며 영원히 대기 상태에 빠지는 데드락 상황이 발생할 수 있다.

 

💡 내부 구현

자바의 synchronized는 JVM이 제공하는 모니터(Monitor)를 기반으로 구현된다. 모든 객체는 하나의 모니터를 가지고 있으며, synchronized 키워드는 이 모니터를 획득하고 해제하는 방식으로 동기화를 수행한다.

synchronized(obj) {
    // 임계 영역
}

 

📖 Thread-Safe의 의미

Thread-Safe

✔️ 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.

✔️ 하나의 함수가 한 스레드로부터 호출되어 실행중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바르게 나오는 것을 말한다.

 

 

Thread-Safe을 지키기 위한 4가지 방법

  • Mutual Exclusion(상호 배제)
    • 한 시점에 하나의 스레드만 공유 자원에 접근하도록 제한하는 것
    • synchronized, Lock, Semaphore
  • Atomic Operation(원자 연산)
    • 중간에 끊기지 않고 한 번에 수행되는 연산
    • AtomicInteger.incrementAndGet()
  • Thread-Local Storage(스레드 지역 저장소)
    • 스레드마다 자기만의 데이터 공간을 갖도록 하는 방법
    • ThreadLocal<T>
  • Re-Entrancy(재진입성)
    • 한 스레드가 이미 점유 중인 락을 자기 자신이 다시 획득할 수 있는 성질
    • ReentrantLock, synchronized

 

📖 가시성과 원자성

 

가시성 문제(Visibility Problem)

가시성 문제는 멀티스레드 프로그래밍에서 한 스레드가 변경한 변수값이 다른 스레드에게 즉시 보이지 않는 현상을 의미한다. 이는 각 스레드가 독립적인 CPU 캐시를 사용하기 때문인데, 한 스레드가 메모리의 값을 변경해도 다른 스레드가 그 변경된 값을 즉시 보지 못할 수 있다. 이러한 상황은 데이터의 일관성을 무너뜨릴 수 있다.

volatile 키워드는 변수의 가시성을 보장하는 데 사용된다. volatile로 선언된 변수는 항상 메인 메모리에서 값을 읽고 쓰도록 보장되며, 어떤 쓰레드가 volatile 변수를 수정하면 그 변경 사항이 즉시 모든 스레드에 반영된다.

 

💡 추가

여러 스레드가 모두 하나의 CPU 코어에서 실행되고, 같은 캐시 메모리를 공유한다면 실제로 가시성 문제는 거의 발생하지 않을 수 있다. 하지만 현실의 대부분 시스템은 멀티코어 구조이기 때문에 스레드가 서로 다른 코어에서 실행될 수 있고, 각 코어는 자기만의 캐시를 갖고 있어서 한 스레드가 변경한 값이 다른 스레드에게 바로 전달되지 않을 수 있다. 이처럼 각 코어의 캐시가 서로 다른 값을 갖고 있을 수 있기 때문에 가시성 문제가 발생하며, 자바에서는 이를 방지하기 위해 volatile이나 synchronized 같은 키워드로 메모리 동기화를 강제해줘야 한다.

 

원자성 문제(Atomicity Problem)

원자성 문제는 멀티스레드 환경에서 하나의 연산이 여러 단계로 나뉘어 수행되기 때문에, 그 중간에 다른 스레드가 끼어들면서 데이터 충돌이 발생하는 문제를 말한다. 예를 들어 count++는 단순해 보이지만 실제로는 “읽기 → 계산 → 저장”의 세 단계로 나뉘며, 이 사이에 다른 스레드가 접근하면 결과가 꼬일 수 있다. 이를 해결하기 위해 자바에서는 synchronized 키워드로 임계 영역을 보호하거나, AtomicInteger와 같은 원자성 보장 클래스를 사용하여 락 없이 안전한 연산을 수행할 수 있도록 지원한다.

 

📖 Atomic

Atomic 하다?

원자적(atomic)이라는 말은 더 이상 나눌 수 없는 최소 단위의 연산이라는 의미를 가지고 있다. 즉, 어떤 스레드가 count++ 같은 연산을 수행할 때, 그 연산이 도중에 다른 스레드에 의해 끼어들 수 없다면 그 연산은 atmoic 하다고 말한다.

 

Atomic 타입

자바에서 제공하는  java.util.concurrent.atomic 패키지의 클래스들이 대표적인 atomic 타입이다. 대표적인 atomic 타입은 다음과 같다.

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference<T>

'Study > CS 스터디 - Java' 카테고리의 다른 글

JVM & GC  (0) 2025.04.09
Thread  (0) 2025.03.27
컬렉션  (0) 2025.03.27
람다, 스트림, 어노테이션  (0) 2025.03.20
문자열, 예외, 제네릭  (0) 2025.03.19
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함