---
description: 동시성 프로그래밍
---

# Week4 Java Concurrency Programming

## 동시성과 병렬성의 차이점이 뭔가요?

자바에서는 여러 스레드가 시분할로 실행되는 것을 동시성이라고 하고, 멀티코어 CPU에서 여러 스레드를 실제로 동시에 실행하는 것을 병렬성이라고 합니다.

동시성:

여러 작업이 논리적으로 동시에 진행되는 것. 여러 작업이 겹쳐서 진행되는 것처럼 보이지만, 실제로는 하나의 프로세서에서 시분할 방식으로 순차적으로 작업이 처리됩니다.

병렬성:&#x20;

여러 작업이 물리적으로 동시에 실행되는 것. 다수의 프로세서가 각각의 작업을 병렬로 실행할때 발생합니다.&#x20;



## Thread-safe하다는게 무슨 뜻인가요?

Thread-safe하다는 것은 여러 스레드가 동시에 특정 코드나 객체에 접근하더라도 그 코드나 객체의 상태가 일관성을 유지하며, 예상치 못한 동작이나 에러가 발생하지 않음을 의미합니다. 동기화, 불변객체, 스레드 전용 객체등의 기법으로 구현할 수 있습니다.

Thread-safe하지 않은 환경에서는 data race가 발생할 수 있습니다. 데이터 레이스는 여러 스레드가 동시에 공유된 자원에 접근하여 읽거나 쓸 때 발생할 수 있는 문제로, 스레드 간의 실행 순서가 불확정적이기 때문에, 한 스레드의 작업중 다른 스레드에 의해 값이 변조될 수 있습니다.



## 가시성 문제와 원자성 문제에 대해 설명해주세요.

가시성 문제는 여러 스레드가 같은 변수를 공유할 때, 한 스레드에서 변경한 값이 다른 스레드에게 즉시 보이지 않을 수 있는 문제입니다. 이는 스레드가 변수 값을 CPU 캐시에 저장하고, 메인 메모리와의 동기화가 제대로 이루어지지 않을 때 발생합니다. 자바에서 volatile/synchronized 키워드를 사용하여 일관성을 유지하여 가시성 문제를 해결할 수 있습니다.&#x20;

원자성 문제는 여러 스레드가 하나의 복합 작업을 동시에 수행할 때 발생할 수 있습니다. 예를 들어 count++ 연산은 총 3단계로 이루어집니다. 이 과정에서 다른 스레드가 개입하면 잘못된 결과가 나올 수 있습니다. 이를 해결하기 위해서는 AtomicInteger와 같은 Atomic 클래스를 사용할 수 있습니다. (내부적으로 CAS 알고리즘을 사용)

### 가시성 문제에 대해 조금 더 자세히 설명해 주세요. 여러 스레드가 모두 한 CPU의 캐시 메모리를 읽으면 가시성 문제가 발생하지 않을 것 같은데, 어떻게 생각하시나요?

가시성 문제는 여러 스레드가 같은 데이터를 읽고 쓸 때, 한 스레드의 변경 사항이 다른 스레드에게 즉시 보이지 않을 수 있는 문제를 말합니다. 이는 주로 CPU 캐시와 메인 메모리 간의 불일치 때문에 발생합니다.&#x20;

각 스레드마다 CPU캐시 메모리를 별도로 들고 있으므로 CPU 캐시 메모리 값이 항상 같을 수 없고, 동기화가 맞춰지기 전까지는 가시성 문제가 발생합니다.

다만, 스레드가 동일한 CPU소켓의 코어에서 실행되면 모든 스레드가 L3 캐시를 바라볼 수 있습니다. 항상 동일 CPU의 캐시 메모리를 읽고, 캐시미스가 발생하여 메인 메모리 값을 읽지 않는 상황에서는 가시성 문제가 발생하지 않을것 같습니다. 다만, 컴파일러가 성능 최적화를 위해 명령어 재정렬을 하는 경우에 코드 실행 순서가 변경되어 가시성 문제가 발생할 수 있습니다.

<details>

<summary> 명령어 재정렬로 인한 가시성 문제</summary>

```java
Thread A
x = 1;  // 명령어 1
y = 1;  // 명령어 2

Thread B
if (y == 1) {  // 명령어 3
    System.out.println(x);  // 명령어 4
}
```

위 상황에서 명령어 순서가 재정렬될 수 있습니다.

```java
Thread A
y = 1;
x = 1; 로 재정렬 됐을때,

int x = 0;
int y = 0;

Thread A
y = 1;
<-------Thread B 실행
if (y == 1) {
    System.out.println(x); // x = 0;
}
x = 1;

```

</details>



## 자바의 동시성 이슈를 해결하는 방법을 아는 만큼 설명해주세요.

자바에서 동시성 문제는 주로 두 가지 측면에서 발생합니다. 가시성 문제, 원자성 문제

자바에서 동시성 이슈를 해결하는 방법은 여러가지 있습니다.

기본적으로 Synchronized 키워드를 사용하여 코드 블록을 동기화하여, 하나의 스레드만 접근하도록 할 수 있는 방법이 있습니다. 다만, 병목 현상으로 인해 성능 저하가 생길 수 있습니다.

두번째로는 불변 객체를 사용하는 것입니다. 상태 변경을 할 수 없기때문에, 여러 스레드가 접근하여도 안전합니다.

세번쨰로는 Volatile을 사용할 수 있습니다. 항상 메인 메모리에서 읽고 쓰도록 할 수 있습니다. Volatile에는 명령어 재정렬이 발생하지 않습니다.

네번째, AtomicInteger 같은 CAS(Compare - and - swap)알고리즘을 사용해 원자성을 보장할 수 있습니다

마지막으로 Concurrent 패키지를 사용하여 스레드 안전한 컬렉션을 사용할 수 있습니다.



## Synchronized 키워드가 뭔가요?

자바에서 데이터 레이스가 발생하지 않도록 동시성 문제를 해결하기 위한 도구입니다. 하나의 스레드만 접근할 수 있도록 보장하고, 다른 스레드들은 해당 메서드나 블록이 모니터 락을 해제할 때까지 대기 상태에 들어가게 됩니다.

특징:

* 상호배제: 한번에 하나씩
* 가시성 보장: 메인 메모리에 기록
* 재진입 가능: Synchronized된 A메서드안에 다른 Synchronized된 B메서드가 있는 상황에서, 재귀적으로 메서드 호출시 새로운 락을 요구하게되면 데드락 발생. Reentrant락을 통해 동일한 락을 다시 획득 안해도된다.&#x20;

<details>

<summary>Java에서 <code>synchronized</code>는 <strong>재진입 락(reentrant lock)</strong> 메커니즘을 지원합니다. 이를 설명하는 예시를 보겠습니다.</summary>

```java
public class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("Inside methodA");
        methodB(); // methodA가 락을 획득한 상태에서 methodB를 호출
    }

    public synchronized void methodB() {
        System.out.println("Inside methodB");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.methodA();
    }
}
```

위 코드에서, `methodA`는 `synchronized`로 보호되고 있으며, `methodA` 내부에서 다시 `methodB`를 호출하고 있습니다. **둘 다 같은 객체에서 락을 요구**하지만, 이 경우에 **재진입 가능성** 덕분에 문제가 발생하지 않습니다.

1. 스레드가 `methodA`를 호출할 때, 그 객체의 락을 획득합니다.
2. `methodA` 내부에서 `methodB`를 호출하는 순간, 스레드는 이미 해당 객체의 락을 가지고 있기 때문에 **재진입 가능성** 덕분에 별다른 추가 락을 기다리지 않고 곧바로 `methodB`를 실행할 수 있습니다.

만약 자바의 락이 **재진입 가능**하지 않다면, `methodA`가 이미 락을 소유한 상태에서 `methodB`를 호출하려고 할 때, 같은 스레드가 동일한 락을 다시 획득할 수 없기 때문에 **데드락(deadlock)** 상태에 빠지게 됩니다. 하지만 재진입 가능성 덕분에 이러한 문제가 발생하지 않습니다.

#### 재진입 가능이 필요한 이유:

* **재귀적 호출**: 재진입 락이 없다면 재귀적 호출에서 문제가 발생할 수 있습니다. 재귀적으로 메서드를 호출할 때마다 새로운 락을 요구할 수 없으므로 재진입 가능성이 있어야 재귀 호출이 올바르게 동작합니다.
* **다중 메서드 호출**: 위 예시처럼 `synchronized`로 보호된 여러 메서드를 호출하는 경우, 이미 락을 소유한 스레드가 다른 메서드를 호출하는 상황에서도 동일한 락을 재획득할 수 있어야 자연스럽게 프로그램이 실행될 수 있습니다.

</details>

<details>

<summary>재진입의 단점</summary>

메서드 A,B,C가 있을때는 각각의 메서드에 스레드가 문제없이 접근이 가능. 하지만 어떠한 메서드를 만들고, 그 메서드 내에 A,B,C를 넣는 순간, 각각의 메서드는 하나의 락으로 관리가됩니다.

그래서 각각의 메서드에 스레드가 문제없이 접근이 가능했던게, 이제는 메서드들이 같은 락을 공유하게 됩니다.

</details>



모니터 락 과정:

* 자바에서 `synchronized` 블록이나 메서드에 접근하려는 스레드는 락을 얻지 못할 경우 **JVM과 운영체제에 의해 대기 상태**로 전환됩니다.
* 이 대기 상태는 `while` 루프와 같은 방식으로 CPU를 지속적으로 사용하면서 확인하는 것이 아니라, **운영체제가 스레드를 일시 정지**시킵니다.
* 락을 소유한 스레드가 `synchronized` 블록을 빠져나오면, JVM은 모니터 대기 큐에서 **대기 중인 스레드 중 하나를 선택**하여 락을 할당합니다.



## Synchronized의 문제점

`synchronized`의 주요 문제점 중 하나는 **병목 현상**으로 인한 **성능 저하**입니다. `synchronized` 블록이나 메서드에 하나의 스레드만 접근할 수 있기 때문에, 다른 스레드들은 대기 상태에 들어가야 하며, 이로 인해 **순차 실행**이 이루어지면서 시스템의 **처리량**이 감소할 수 있습니다.

또한, **데드락**의 위험이 있습니다. 여러 스레드가 서로의 락을 기다리면서 무한 대기 상태에 빠질 수 있으며, 이는 프로그램이 멈추는 원인이 됩니다.&#x20;

**공정성** 문제도 발생할 수 있는데, 대기 중인 스레드들 중에서 어떤 스레드가 락을 먼저 획득할지에 대한 보장이 없기 때문에 특정 스레드가 지속적으로 락을 얻지 못하는 **기아(Starvation)** 상황이 발생할 수 있습니다.



## Synchronized는 어떻게 구현되어 있나요?

&#x20;Synchronized는 자바의 JVM 수준에서 구현된 동기화 메커니즘입니다. JVM은 모니터를 활용하여 동기화를 처리합니다.

모든 객체는 암묵적으로 모니터를 가지고 있고, 스레드가 synchronized 블록이나 메서드에 진입하려고 하면, JVM은 해당 객체의 모니터 락을 획득하려고 시도합니다. 다른 스레드가 이미 가지고 있다면, 대기 상태로 전환됩니다.

* JVM은 `synchronized` 블록의 시작 부분에서 **Monitor Enter** 명령어를 실행하고, 종료 부분에서 **Monitor Exit** 명령어를 실행합니다.
* **Monitor Enter**는 스레드가 모니터 락을 획득하려고 시도하며, 성공하면 스레드는 블록 내부의 코드를 실행할 수 있습니다.
* **Monitor Exit**는 스레드가 모니터 락을 해제하고, 다른 대기 중인 스레드가 락을 획득할 수 있도록 합니다.'



## atomic하다는 것이 무슨 의미인가요?

원자성(atomicity)는 특정 작업이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미합니다. 하나의 작업이 완전히 실행되거나 전혀 실행되지 않는 상태를 의미합니다. 이는 작업이 도중에 중단되거나 다른 작업이 끼어들 수 없는 것을 보장합니다.

데이터베이스에서의 원자성: 모두 실행되거나 아무것도 실행되지 않는 특성

동시성 프로그래밍에서의 원자성: 특정 작업이 여러 단계로 이루어져 있어도, 하나의 단위 작업처럼 실행되어 다른 스레드가 간섭할 수 없는 것.



## CAS 알고리즘에 대해 설명해주세요

java.util.concurret 패키지에서 제공하는 atomic 클래스들이 내부적으로 사용하는 알고리즘입니다. 동기화 없이 여러 스레드가 안전하게 공유 데이터를 수정할 수 있도록 도와주는 알고리즘입니다.

메모리 주소, 예상되는 값, 새로운 값을 사용하여 동작합니다.

1. 특정 메모리 위치에서 현재 값을 읽습니다. (예상되는 값. 스레드가 메모리 위치에서 값을 읽은 순간 존재하는 값)
2. 업데이트를 위한 새로운 값을 연산
3. 기존 값을 업데이트 하기전에 다시 한번 확인하고, 새로운 값으로 업데이트합니다.



## Vector, HashTable, Collections.synchronizedXXX의 문제점이 뭔가요?

Vector, HashTable, Collections.synchronized 등은 모두 synchronized 키워드를 사용하여 동기화를 처리합니다. 이로 인해 전체 메서드에 락이 걸리게 되며, 하나의 스레드만 해당 메서드를 실행합니다. 전체 메서드에 대해 동기화를 적용하여, 동기화가 필요하지 않은 경우에도 동기화가 발생할 수 있습니다.

자바 5 이후부터는 ConcurrentHashMap과 같은 더 효율적인 대체 기술들이 등장하여, 기존 동기화 컬렉션들은 잘 사용되지 않습니다. 락 분할을 사용하여 특정 데이터가 저장된 버킷(슬롯)에만 락이 걸립니다.

<details>

<summary>Example</summary>

```java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);  // "apple" 키에 해당하는 버킷에 락이 걸림
map.put("banana", 2); // "banana" 키에 해당하는 다른 버킷에 락이 걸림
```

* `apple`과 `banana`는 각각 다른 해시 값을 가지며, 다른 버킷에 저장됩니다.
* 스레드 A가 `apple`에 대해 작업할 때, 스레드 B는 `banana`에 대해 작업할 수 있습니다. 두 작업은 서로 다른 버킷에서 이루어지므로 **동시에 실행**될 수 있습니다.

</details>



## &#x20;SynchronizedList와 CopyOnWriteArrayList의 차이

Thread-safe한 리스트를 제공하기 위해 사용됩니다.

SyncrhoinzedList는 내부적으로 synchronized 블록이 사용됩니다.

CopyOnWriteArrayList는 쓰기 작업이 발생할 떄마다 전체 배열의 복사본을 생성합니다. 읽기 작업은 기존 배열을 참조하므로, 동기화 없이도 안전하게 동시 읽기 작업이 가능합니다. 쓰기 작업이 많아지면 메모리 사용량이 많아지는게 단점이라서 읽기 작업이 빈번하고 쓰기 작업은 적은 경우에 적합합니다.



## ConcurrentHashMap의 동작 과정을 SynchronizedMap과 비교하여 설명해주세요

**SynchronizedMap**은 모든 작업에 대해 **맵 전체에 단일 락**을 사용하여 동기화를 보장합니다. 이로 인해 병목 현상과 성능 저하가 발생할 수 있으며, 특히 동시성 처리가 중요한 환경에서는 확장성이 제한적입니다.

반면, **ConcurrentHashMap**은 **락 분할(Lock Stripping)** 기법을 사용하여, 맵 전체가 아닌 **개별 버킷에 대한 락**을 걸어 동기화를 처리합니다. 이를 통해 여러 스레드가 동시에 다른 버킷에 접근할 수 있으며, 특히 읽기 작업이 비동기적으로 수행되므로 성능이 뛰어납니다. 이로 인해 **동시성 처리 성능이 크게 향상**되며, 확장성이 뛰어납니다.
