1. 동시성(Concurrency)이란?
동시성은 여러 스레드가 동시에 실행될 때 발생합니다.
각 스레드는 독립적으로 작업을 수행하지만, 때때로 공유 자원(예: 데이터베이스)에 접근할 필요가 있습니다.
이때, 올바른 동시성 관리 없이 여러 스레드가 동시에 같은 자원에 접근하면 데이터 무결성을 위협하고 예측 불가능한 결과를 초래할 수 있습니다.
동시성 문제를 해결하지 못하면 데이터 불일치, 교착 상태(Deadlock), 성능 저하 등의 문제가 발생할 수 있습니다.
정확하고 효율적인 데이터 처리를 위해 이러한 문제들을 관리하는 것이 필요합니다.
2. 원자성과 가시성
1) 원자성(Atomicity)
원자성은 어떤 작업이 '전부 아니면 전혀'의 상태로만 존재한다는 원칙입니다.
은행 계좌에서 돈을 이체할 때로 예를 들면,
돈이 한 계좌에서 빠져나가 다른 계좌로 들어가는 과정은 반드시 완전히 이루어지거나, 전혀 이루어지지 않아야 합니다.
이러한 원자성은 데이터의 일관성과 정확성을 보장하는 데 필수적입니다.
2) 가시성(Visibility)
가시성은 한 프로세스나 스레드에 의해 이루어진 변경사항이 다른 프로세스나 스레드에 어떻게 '보여지는지'를 나타냅니다.
가시성이 확보되지 않으면, 한 스레드가 데이터를 변경했을 때 다른 스레드가 그 변경사항을 즉시 보지 못할 수 있습니다.
즉, 데이터 불일치를 초래하고 시스템의 안정성을 해칠 수 있습니다.
3. Java 에서 동시성을 제어하는 방법
1) synchronized
synchronized 키워드는 메서드나 블록을 하나의 스레드만 접근할 수 있도록 제한합니다.
이러한 방법은 메서드 또는 객체에 대한 독점적인 접근을 제공하여 데이터 무결성을 보장합니다.

https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
2) volatile
volatile 키워드는 변수를 메인 메모리에 저장하도록 함으로써, 모든 스레드가 항상 변수의 최신 값을 볼 수 있도록 보장합니다. (가시성 보장)
멀티스레딩 환경에서, 각 스레드는 변수의 복사본을 자신의 작업 메모리(CPU 캐시)에 저장하고 사용할 수 있습니다.
이 경우, 한 스레드가 변수의 값을 변경해도, 다른 스레드는 이 변경을 즉시 볼 수 없을 때가 있습니다.
volatile 키워드는 이러한 가시성 문제를 해결해 줍니다.
하지만, 한 번에 여러 스레드가 같은 변수를 변경하려고 할 때, 그 변경 작업이 안전하게 이루어지는 것은 보장하지 않습니다. (원자성 보장 X)
3) Atomic
java.util.concurrent.atomic 패키지는 원자적 연산을 지원하는 클래스들을 제공합니다.
- 원자성 보장: Atomic 클래스들은 멀티스레딩 환경에서도 데이터의 원자성을 보장합니다. 즉, 여러 스레드가 동시에 같은 변수를 수정하더라도 데이터 불일치 문제를 피할 수 있습니다.
- 락-프리(lock-free) 연산: 대부분의 Atomic 연산들은 락을 사용하지 않고, CPU의 원자적 인스트럭션(Atomic Operation, 나눌 수 없는 하나의 작업)을 사용하여 실행됩니다. 이러한 방식은 성능을 향상시키고, 데드락(deadlock)을 방지합니다.
- 가시성 보장: Atomic 변수는 모든 스레드에게 최신 값이 보이도록 합니다. (volatile 변수와 유사한 메커니즘을 사용합니다.)
Atomic은 원자성도 보장하고, 가시성도 보장하면서 Lock-Free 이기 때문에 성능 향상도 되면서, 데드락도 발생하지 않는다는 것인데 어떻게 이것들이 가능하게 되는 것인지 알아보겠습니다.
Atomic의 내부적인 동작
- Compare-and-Swap (CAS): Atomic 클래스들의 핵심은 CAS 연산입니다. CAS 연산은 세 부분으로 구성됩니다
현재 값, 예상 값, 새로운 값. 이 연산은 현재 값이 예상 값과 일치할 경우에만 새로운 값을 저장합니다.
- 현재 값 읽기: 메모리에서 변수의 현재 값을 읽습니다.
- 예상 값 비교: 현재 값이 예상 값(원래 값)과 같은지 확인합니다.
(연산이 시작된 이후 다른 스레드에 의해 값이 변경되었는지를 판단하는 데 사용됩니다.) - 값 교체: 만약 현재 값이 예상 값과 동일하다면, 변수를 새로운 값으로 업데이트합니다.
- 원자성 보장: 위의 모든 단계는 단일, 불가분의 연산으로 수행됩니다. 즉, 이 과정 중에는 다른 스레드가 개입할 수 없으며, 연산은 전체적으로 완료되거나 전혀 수행되지 않습니다.
- Busy-Wait Loops: CAS 연산이 실패하면, 대부분의 Atomic 클래스들은 반복적으로 CAS를 시도합니다. 이는 스핀락(spinlock) 또는 바쁜 대기(busy-wait)라고 불립니다.
java 코드로 Atomic이 어떻게 구성되어 있는지 확인해 보겠습니다.

AtomicInteger를 예시로 보면, 값을 설정하는 value 변수에 volatile 키워드가 사용된 것을 확인할 수 있습니다.
volatile 키워드는 위에서 설명한 것과 같이 가시성을 보장해 줍니다.
이번에는 원자성을 어떻게 보장하는지 알아보겠습니다.
먼저 incrementAndGet()이라는 메서드를 확인해 보면 getAndAddInt()를 호출하고 있고,

getAndAddInt() 메서드는 다음과 같이 되어있습니다.

CAS 연산이 성공할 때까지 (즉, 값이 성공적으로 업데이트될 때까지) 계속 실행되는 것을 확인할 수 있습니다.
이를 통해 원자성을 보장하게 됩니다.
그런데 위에서는 Compare-And-Swap이라고 하였는데,
java 코드에서는 CompareAndSet으로 표현되고 있는 것을 확인할 수 있습니다.
CompareAndSet이라고 표현한 이유에 대해서 알아보겠습니다.

https://en.wikipedia.org/wiki/Compare-and-swap
CAS연산의 결과는 대체를 수행했는지 여부를 나타내야 하며, 단순한 boolean 응답 또는 메모리 위치에서 읽은 값을 반환할 수 있습니다.
boolean을 반환할 경우에는 compare-and-set이라고 불리기도 합니다.
java에서는 boolean 값으로 연산의 성공 여부를 판단하고 있기 때문에 CompareAndSet이라는 변수를 사용한 것입니다.
4. 성능 비교
테스트 코드로 동시성을 제어하지 않은 경우와 synchronized 키워드, volatile 키워드, atomic을 사용했을 때 성능을 확인해 보겠습니다.
1) 동시성을 제어하지 않은 경우



동시성을 제어하지 않고 멀티스레드 환경에서 연산을 하였을 경우에는 예상했던 결과가 나오지 않는 것을 확인할 수 있습니다.
2) synchronized 키워드를 사용한 경우



synchronized 키워드를 사용한 경우에는 동시성이 제어가 된 것을 확인할 수 있습니다.
하지만 Thread.sleep(1)로 count를 1 증가시킬 때마다 1ms 동안 sleep을 주었더니 최종적으로 완료되는데 2분 20초라는 시간이 걸리는 것을 확인할 수 있습니다.
synchronized 키워드는 Blocking 방식이기 때문에 하나의 스레드만 접근이 가능하기에 이러한 결과가 나오게 된 것입니다.
그렇기 때문에 synchronized 키워드는 성능상 문제가 생길 수 있다는 것을 항상 염두에 두어야 합니다.
3) volatile 키워드를 사용한 경우



위에서 설명했듯이 volatile 은 가시성은 보장하지만, 원자성은 보장해주지 못한다고 하였습니다.
그렇기 때문에 동시성이 제어되지 못하여 예상한 결과가 나오지 않은 것을 확인할 수 있습니다.
4) Atomic을 사용한 경우



Atomic 은 volatile을 사용해서 가시성을 확보하고, CAS 연산을 통해 원자성을 확보한다고 하였습니다.
그렇기 때문에 동시성 문제도 생기지 않은 것을 확인할 수 있습니다.
또한 CAS 연산을 통해 lock-free이기 때문에 synchronized와 같이 Tread.sleep(1)을 걸었음에도 1361ms 밖에 걸리지 않은 것을 확인할 수 있습니다.
5. 정리
동시성(Concurrency)은 여러 스레드가 동시에 실행될 때 발생합니다.
→ 공유 자원에 대한 접근을 필요로 하며, 올바른 관리 없이는 데이터 무결성 문제나 예측 불가능한 결과를 초래할 수 있습니다.
원자성(Atomicity)은 어떤 작업이 완전히 수행되거나 전혀 수행되지 않는 것을 의미합니다.
→ 데이터의 일관성과 정확성을 보장하는 데 중요합니다.
가시성(Visibility)은 한 스레드에 의한 변경사항이 다른 스레드에 어떻게 보여지는지를 나타냅니다.
→ 가시성이 없으면, 스레드 간 데이터 불일치 문제가 발생할 수 있습니다.
동시성 제어 방법
- synchronized: 메서드나 블록에 대한 독점적인 접근을 제공하여 동시성 문제를 해결합니다.
→ 하지만 성능 저하의 문제가 있을 수 있습니다. (Blocking) - volatile: 변수의 가시성을 보장하지만, 원자성은 보장하지 않습니다.
→ 가시성 문제만 해결할 수 있으며, 원자성 문제는 해결하지 못합니다. (동시성 문제 해결 X) - Atomic: java.util.concurrent.atomic 패키지는 원자적 연산을 지원하며, 락-프리(lock-free) 연산을 제공합니다.
→ 원자성과 가시성을 동시에 보장하며 CAS 연산을 통해 성능상의 이점을 제공합니다. (Non-Blocking)
'Java' 카테고리의 다른 글
Java의 ArrayList는 어떻게 동적으로 크기를 증가시키는 것인가? (0) | 2023.12.16 |
---|
1. 동시성(Concurrency)이란?
동시성은 여러 스레드가 동시에 실행될 때 발생합니다.
각 스레드는 독립적으로 작업을 수행하지만, 때때로 공유 자원(예: 데이터베이스)에 접근할 필요가 있습니다.
이때, 올바른 동시성 관리 없이 여러 스레드가 동시에 같은 자원에 접근하면 데이터 무결성을 위협하고 예측 불가능한 결과를 초래할 수 있습니다.
동시성 문제를 해결하지 못하면 데이터 불일치, 교착 상태(Deadlock), 성능 저하 등의 문제가 발생할 수 있습니다.
정확하고 효율적인 데이터 처리를 위해 이러한 문제들을 관리하는 것이 필요합니다.
2. 원자성과 가시성
1) 원자성(Atomicity)
원자성은 어떤 작업이 '전부 아니면 전혀'의 상태로만 존재한다는 원칙입니다.
은행 계좌에서 돈을 이체할 때로 예를 들면,
돈이 한 계좌에서 빠져나가 다른 계좌로 들어가는 과정은 반드시 완전히 이루어지거나, 전혀 이루어지지 않아야 합니다.
이러한 원자성은 데이터의 일관성과 정확성을 보장하는 데 필수적입니다.
2) 가시성(Visibility)
가시성은 한 프로세스나 스레드에 의해 이루어진 변경사항이 다른 프로세스나 스레드에 어떻게 '보여지는지'를 나타냅니다.
가시성이 확보되지 않으면, 한 스레드가 데이터를 변경했을 때 다른 스레드가 그 변경사항을 즉시 보지 못할 수 있습니다.
즉, 데이터 불일치를 초래하고 시스템의 안정성을 해칠 수 있습니다.
3. Java 에서 동시성을 제어하는 방법
1) synchronized
synchronized 키워드는 메서드나 블록을 하나의 스레드만 접근할 수 있도록 제한합니다.
이러한 방법은 메서드 또는 객체에 대한 독점적인 접근을 제공하여 데이터 무결성을 보장합니다.

https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
2) volatile
volatile 키워드는 변수를 메인 메모리에 저장하도록 함으로써, 모든 스레드가 항상 변수의 최신 값을 볼 수 있도록 보장합니다. (가시성 보장)
멀티스레딩 환경에서, 각 스레드는 변수의 복사본을 자신의 작업 메모리(CPU 캐시)에 저장하고 사용할 수 있습니다.
이 경우, 한 스레드가 변수의 값을 변경해도, 다른 스레드는 이 변경을 즉시 볼 수 없을 때가 있습니다.
volatile 키워드는 이러한 가시성 문제를 해결해 줍니다.
하지만, 한 번에 여러 스레드가 같은 변수를 변경하려고 할 때, 그 변경 작업이 안전하게 이루어지는 것은 보장하지 않습니다. (원자성 보장 X)
3) Atomic
java.util.concurrent.atomic 패키지는 원자적 연산을 지원하는 클래스들을 제공합니다.
- 원자성 보장: Atomic 클래스들은 멀티스레딩 환경에서도 데이터의 원자성을 보장합니다. 즉, 여러 스레드가 동시에 같은 변수를 수정하더라도 데이터 불일치 문제를 피할 수 있습니다.
- 락-프리(lock-free) 연산: 대부분의 Atomic 연산들은 락을 사용하지 않고, CPU의 원자적 인스트럭션(Atomic Operation, 나눌 수 없는 하나의 작업)을 사용하여 실행됩니다. 이러한 방식은 성능을 향상시키고, 데드락(deadlock)을 방지합니다.
- 가시성 보장: Atomic 변수는 모든 스레드에게 최신 값이 보이도록 합니다. (volatile 변수와 유사한 메커니즘을 사용합니다.)
Atomic은 원자성도 보장하고, 가시성도 보장하면서 Lock-Free 이기 때문에 성능 향상도 되면서, 데드락도 발생하지 않는다는 것인데 어떻게 이것들이 가능하게 되는 것인지 알아보겠습니다.
Atomic의 내부적인 동작
- Compare-and-Swap (CAS): Atomic 클래스들의 핵심은 CAS 연산입니다. CAS 연산은 세 부분으로 구성됩니다
현재 값, 예상 값, 새로운 값. 이 연산은 현재 값이 예상 값과 일치할 경우에만 새로운 값을 저장합니다.
- 현재 값 읽기: 메모리에서 변수의 현재 값을 읽습니다.
- 예상 값 비교: 현재 값이 예상 값(원래 값)과 같은지 확인합니다.
(연산이 시작된 이후 다른 스레드에 의해 값이 변경되었는지를 판단하는 데 사용됩니다.) - 값 교체: 만약 현재 값이 예상 값과 동일하다면, 변수를 새로운 값으로 업데이트합니다.
- 원자성 보장: 위의 모든 단계는 단일, 불가분의 연산으로 수행됩니다. 즉, 이 과정 중에는 다른 스레드가 개입할 수 없으며, 연산은 전체적으로 완료되거나 전혀 수행되지 않습니다.
- Busy-Wait Loops: CAS 연산이 실패하면, 대부분의 Atomic 클래스들은 반복적으로 CAS를 시도합니다. 이는 스핀락(spinlock) 또는 바쁜 대기(busy-wait)라고 불립니다.
java 코드로 Atomic이 어떻게 구성되어 있는지 확인해 보겠습니다.

AtomicInteger를 예시로 보면, 값을 설정하는 value 변수에 volatile 키워드가 사용된 것을 확인할 수 있습니다.
volatile 키워드는 위에서 설명한 것과 같이 가시성을 보장해 줍니다.
이번에는 원자성을 어떻게 보장하는지 알아보겠습니다.
먼저 incrementAndGet()이라는 메서드를 확인해 보면 getAndAddInt()를 호출하고 있고,

getAndAddInt() 메서드는 다음과 같이 되어있습니다.

CAS 연산이 성공할 때까지 (즉, 값이 성공적으로 업데이트될 때까지) 계속 실행되는 것을 확인할 수 있습니다.
이를 통해 원자성을 보장하게 됩니다.
그런데 위에서는 Compare-And-Swap이라고 하였는데,
java 코드에서는 CompareAndSet으로 표현되고 있는 것을 확인할 수 있습니다.
CompareAndSet이라고 표현한 이유에 대해서 알아보겠습니다.

https://en.wikipedia.org/wiki/Compare-and-swap
CAS연산의 결과는 대체를 수행했는지 여부를 나타내야 하며, 단순한 boolean 응답 또는 메모리 위치에서 읽은 값을 반환할 수 있습니다.
boolean을 반환할 경우에는 compare-and-set이라고 불리기도 합니다.
java에서는 boolean 값으로 연산의 성공 여부를 판단하고 있기 때문에 CompareAndSet이라는 변수를 사용한 것입니다.
4. 성능 비교
테스트 코드로 동시성을 제어하지 않은 경우와 synchronized 키워드, volatile 키워드, atomic을 사용했을 때 성능을 확인해 보겠습니다.
1) 동시성을 제어하지 않은 경우



동시성을 제어하지 않고 멀티스레드 환경에서 연산을 하였을 경우에는 예상했던 결과가 나오지 않는 것을 확인할 수 있습니다.
2) synchronized 키워드를 사용한 경우



synchronized 키워드를 사용한 경우에는 동시성이 제어가 된 것을 확인할 수 있습니다.
하지만 Thread.sleep(1)로 count를 1 증가시킬 때마다 1ms 동안 sleep을 주었더니 최종적으로 완료되는데 2분 20초라는 시간이 걸리는 것을 확인할 수 있습니다.
synchronized 키워드는 Blocking 방식이기 때문에 하나의 스레드만 접근이 가능하기에 이러한 결과가 나오게 된 것입니다.
그렇기 때문에 synchronized 키워드는 성능상 문제가 생길 수 있다는 것을 항상 염두에 두어야 합니다.
3) volatile 키워드를 사용한 경우



위에서 설명했듯이 volatile 은 가시성은 보장하지만, 원자성은 보장해주지 못한다고 하였습니다.
그렇기 때문에 동시성이 제어되지 못하여 예상한 결과가 나오지 않은 것을 확인할 수 있습니다.
4) Atomic을 사용한 경우



Atomic 은 volatile을 사용해서 가시성을 확보하고, CAS 연산을 통해 원자성을 확보한다고 하였습니다.
그렇기 때문에 동시성 문제도 생기지 않은 것을 확인할 수 있습니다.
또한 CAS 연산을 통해 lock-free이기 때문에 synchronized와 같이 Tread.sleep(1)을 걸었음에도 1361ms 밖에 걸리지 않은 것을 확인할 수 있습니다.
5. 정리
동시성(Concurrency)은 여러 스레드가 동시에 실행될 때 발생합니다.
→ 공유 자원에 대한 접근을 필요로 하며, 올바른 관리 없이는 데이터 무결성 문제나 예측 불가능한 결과를 초래할 수 있습니다.
원자성(Atomicity)은 어떤 작업이 완전히 수행되거나 전혀 수행되지 않는 것을 의미합니다.
→ 데이터의 일관성과 정확성을 보장하는 데 중요합니다.
가시성(Visibility)은 한 스레드에 의한 변경사항이 다른 스레드에 어떻게 보여지는지를 나타냅니다.
→ 가시성이 없으면, 스레드 간 데이터 불일치 문제가 발생할 수 있습니다.
동시성 제어 방법
- synchronized: 메서드나 블록에 대한 독점적인 접근을 제공하여 동시성 문제를 해결합니다.
→ 하지만 성능 저하의 문제가 있을 수 있습니다. (Blocking) - volatile: 변수의 가시성을 보장하지만, 원자성은 보장하지 않습니다.
→ 가시성 문제만 해결할 수 있으며, 원자성 문제는 해결하지 못합니다. (동시성 문제 해결 X) - Atomic: java.util.concurrent.atomic 패키지는 원자적 연산을 지원하며, 락-프리(lock-free) 연산을 제공합니다.
→ 원자성과 가시성을 동시에 보장하며 CAS 연산을 통해 성능상의 이점을 제공합니다. (Non-Blocking)
'Java' 카테고리의 다른 글
Java의 ArrayList는 어떻게 동적으로 크기를 증가시키는 것인가? (0) | 2023.12.16 |
---|