[Java] 동시성 프로그래밍

N’oublie pas de t’aimer·2025년 1월 22일

Java

목록 보기
13/18

동시성 vs 병렬성

동시성 (Concurrency) vs 병렬성 (Parallelism)
동시성과 병렬성은 두 단어 모두 말 그대로 동시에 하는 것이 아닌가 생각이 들어서 혼동하기 쉽다.

동시성 (병행성)

  • 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번걸아가며 실행하는 성질이다.
  • CPU 하나가 Time Sharing 기법을 통해 실제로 동시에 스레드가 실행되는 것은 아니지만, CPU 제어권을 매우 빠르게 스레드에게 줬다가 뺏으면서 사람이 보기에 마치 동시에 실행되는 것처럼 보이는 것을 뜻한다.
    병렬성
  • 멀티 작업을 위해 멀티 코어에서 개별 스레드를 할당 받아 동시에 실행하는 성질이다.
  • 정말 말 그대로 CPU 각자가 나뉘어서 각자의 일을 하여 실질적인 동시 작업을 수행하는 것을 뜻한다.

thread-safe

Thread-safe하다는 것은 여러 스레드가 동시에 동일한 객체나 메서드에 접근하더라도 프로그램이 의도한 대로 동작하고 데이터의 일관성이 유지됨을 보장한다는 의미이다.

멀티스레드 환경에서는 여러 스레드가 같은 자원(예: 객체, 변수 등)을 동시에 읽거나 쓰는 경우가 많다. 이때, 자원을 안전하게 관리하지 않으면 예상치 못한 동작(예: 데이터 손실, 불일치, 경합 상태)이 발생할 수 있다. Thread-safe는 이런 문제를 방지하는 설계 또는 구현을 의미한다.

동시성 프로그래밍에서 발생할 수 있는 문제점

컴퓨터의 CPU와 RAM의 관계도를 그려보면 다음과 같은 그림이 될 것이다.

CPU가 어떤 작업을 처리하기 위해 데이터가 필요할 때, CPU는 RAM의 일부분을 고속의 저장 장치인 CPU Cache Memory로 읽어들인다. 이 읽어들인 데이터로 명령을 수행하고, RAM에 저장하기 위해서는 데이터를 CPU Cache Memory에 쓴 다음 RAM에 쓰기 작업을 수행한다. 그러나 CPU가 캐시에 쓰기 작업을 수행했다고 해서 바로 RAM으로 쓰기 작업을 수행하지 않는다. 반대로 읽기 작업도 해당 데이터가 RAM에서 변경이 되었다고 해도, 언제 CPU Cache Memory가 아닌 RAM에서 데이터를 읽어 들여서 CPU Cache Memory를 업데이트할 지 보장하지 않는다.

동시성 프로그래밍에서는 CPU와 RAM의 중간에 위치하는 CPU Cache Memory와 병렬성이라는 특징때문에 다수의 스레드가 공유 자원에 접근할 때 두 가지 문제가 발생할 수 있다.

  • 가시성 문제
  • 원자성(동시 접근) 문제

사실 위의 두 문제는 동시성보다는 병렬성때문에 발생하는 문제이지만, 자바 스레드는 동시성의 성질을 가지고 있으므로, 자바에서는 동시성 프로그래밍에서 발생하는 문제점이라고 부른다.

가시성 문제

여러 개의 스레드가 사용됨에 따라, CPU Cache Memory와 RAM의 데이터가 서로 일치하지 않아 생기는 문제를 의미한다. 이를 해결하기 위해서는 가시성이 보장되어야 하는 변수를 CPU Cache Memory가 아니라 RAM에서 바로 읽도록 보장해야 한다.

이때 변수에 volatile 키워드를 붙임으로써 가시성을 보장할 수 있다.

private static volatile boolean isStop;

그러나 가시성만 보장된다고 동시성이 보장되는 것은 아니다. 간단한 예를 살펴 보자.

전철 비용을 계산하는 프로그램을 작성한다고 가정하겠다. 이때 나이에 따라서 70세 미만과 70세 이상의 표 값이 다른 상황이고, 날짜를 실시간으로 반영하여 비용을 계산해야 한다.

각 스레드의 역할은 다음과 같다.

Thread 1

  • 고객의 나이를 읽는다.
  • 읽어온 나이를 기준으로 비용을 계산한다.
  • 비용을 반환한다.

Thread 2

  • 현재 연도를 지속적으로 읽는다.
  • 해가 바뀌면 고객의 나이를 계산한다.
  • 바뀐 고객의 나이를 저장한다.

volatile을 통해 가시성을 해결했지만, 해가 바뀌는 시점에 문제가 발생한다.

  • 나이가 69세인 고객이 계산을 진행할 때, 첫 번째 스레드에서는 RAM에서 나이가 69세임을 가져와 나이에 따라 비용을 계산하는 메소드를 실행한다.
  • 그러나 마침 해가 딱 바뀌어 년 바뀜을 계산하는 두 번째 스레드에서 RAM에 해당 고객의 나이를 70살로 수정한다.
  • 첫 번째 스레드는 나이가 70세로 바뀐 것을 모르고 69세 기준으로 전철 비용을 계산하여 잘못된 비용을 반환한다.

위 예제에서 알 수 있듯이, 가시성이 보장된다고 동시성이 보장되는 것은 아니다. volatile 키워드는 어디까지나 volatile 변수를 메인 메모리로부터 읽을 수 있게 해 주는 것이 전부이고, 다른 스레드에 의해 이 값이 언제든 바뀔 수 있다. 즉, 가시성이란 공유 데이터를 읽는 경우의 동시성만 보장하는 것이라 생각하면 된다.

원자성 문제

원자성은 가시성과 멀티 스레드 환경에서 스레드간 공유 메모리 이슈를 발생시킨다는 점에서 공통점이 있다. 하지만 시스템 관점에서 보면 두 개념은 다르다.

가시성

  • CPU - Cache - Memory 관계 상의 개념

원자성

  • 한 줄의 프로그램 문장이 컴파일러에 의해 기계어로 변경되면서, 이를 기계가 순차적으로 처리하기 위한 여러 개의 Machine Instruction이 만들어져 실행되기 때문에 일어나는 현상
  • 예를 들어 프로그램 언어적으로 i++ 문장은 다음과 같은 기계가 수행하는 명령어로 쪼개진다.
    - i를 메모리로부터 읽는다.
    - 읽은 값에 1을 더한다.
    - 연산한 값을 메모리에 저장한다.
  • 멀티 스레드 환경에서는 한 스레드가 각 기계 명령어를 수행하는 동안에 다른 스레드가 개입하여 공유 변수에 접근하여 같은 기계 명령어를 수행할 수 있으므로 값이 꼬이게 된다. (race condition)

원자성 문제를 해결하기 위해서는 synchronized 또는 atomic을 사용해야 한다.
참고로 원자성 문제를 synchronized 또는 atomic을 통해 해결한다면 가시성의 문제도 해결된다. synchronized 블럭을 들어가기 전에 CPU Cache Memory와 Main Memory를 동기화 해주며, atomic의 경우에는 CAS 알고리즘에 의해 원자성 문제와 CPU Cache Memory에 잘못된 값을 참조하는 문제를 동시에 해결해주기 때문이다.

profile
매일 1퍼센트씩 나아지기 ୧(﹒︠ ̫ ̫̊ ̫﹒︡)୨

0개의 댓글