본문 바로가기

미국유학/CS 601 Principles of Software Dev

241003 PSD - Multithreading, Synchronization

`andThen()`은 **Java**의 **`Consumer` 인터페이스**에서 제공하는 메소드 중 하나로, 두 개의 `Consumer`를 **연결**하여 **순차적으로** 실행할 수 있게 해주는 메소드입니다.

### 기본적인 `Consumer` 설명:

- **`Consumer<T>`**는 **하나의 입력**을 받아서 어떤 동작을 수행하는 **함수형 인터페이스**입니다. 즉, 값을 받아서 처리하지만 그 값을 반환하지 않습니다. 
- **주로 `accept(T t)` 메소드를 통해 동작**합니다. 이 메소드는 입력을 받아서 그 값으로 어떤 처리를 수행하지만 결과를 반환하지 않는 메소드입니다.

### `andThen()` 메소드의 역할:

- **`andThen(Consumer<? super T> after)`**는 **두 개의 `Consumer`를 연결**하여 **순차적으로 실행**되게 만듭니다.
- 첫 번째 `Consumer`가 **처리한 후**에 두 번째 `Consumer`가 **같은 입력으로 작업을 수행**하게 됩니다.
  
즉, 하나의 입력 값으로 두 가지 작업을 차례대로 실행할 때 사용합니다.

### 주요 특징:
1. **순차 실행**: `andThen()` 메소드는 먼저 현재 `Consumer`의 작업을 수행한 후, `after`에 전달된 `Consumer`의 작업을 수행합니다.
2. **값 전달**: 두 `Consumer`는 동일한 입력 값을 사용합니다. 첫 번째 `Consumer`가 값을 변경하거나 다른 값을 생성해서 전달하지는 않습니다.

### 예시 코드:

```java
import java.util.function.Consumer;

public class ConsumerAndThenExample {
    public static void main(String[] args) {
        // 첫 번째 Consumer: 문자열을 출력
        Consumer<String> printConsumer = (str) -> System.out.println("Print: " + str);
        
        // 두 번째 Consumer: 문자열의 길이를 출력
        Consumer<String> lengthConsumer = (str) -> System.out.println("Length: " + str.length());

        // andThen으로 두 Consumer를 연결
        Consumer<String> combinedConsumer = printConsumer.andThen(lengthConsumer);

        // "Hello"라는 입력값으로 두 작업이 순차적으로 실행됨
        combinedConsumer.accept("Hello");
    }
}
```

### 출력 결과:
```
Print: Hello
Length: 5
```

### 설명:
1. **첫 번째 `Consumer`** (`printConsumer`)는 `"Hello"`를 출력합니다.
2. **두 번째 `Consumer`** (`lengthConsumer`)는 `"Hello"`의 길이인 `5`를 출력합니다.
3. **`andThen()`** 메소드로 두 `Consumer`가 **결합**되었기 때문에, 첫 번째 `Consumer`가 실행된 후 두 번째 `Consumer`가 순차적으로 실행됩니다.

### 흐름 정리:
- **`printConsumer.accept("Hello")`**가 먼저 실행되어 `"Hello"`가 출력됩니다.
- 그 다음으로 **`lengthConsumer.accept("Hello")`**가 실행되어 문자열의 길이 `5`가 출력됩니다.
- 두 `Consumer`가 동일한 입력값 `"Hello"`를 사용하여 각각의 작업을 수행합니다.

### 요약:
- **`andThen()`**은 `Consumer` 인터페이스의 메소드로, **두 `Consumer`를 연결하여 순차적으로 실행**하게 해줍니다.
- 첫 번째 `Consumer`가 작업을 완료하면, **같은 입력 값**으로 두 번째 `Consumer`가 실행됩니다.
- 이를 통해 **연속적인 작업**을 효율적으로 구성할 수 있습니다.

 

 

 

==============================================================

 

 

public class StopThread {
    private static boolean stopRequested = false;

    public static void main (String[] args) {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                 i++;
                //System.out.println(i);
                //System.out.println(stopRequested);
            } // while
            //System.out.println(stopRequested);
        }); //Thread

        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        }
        catch (InterruptedException e) {
            System.out.println("Thread was interrupted.");
        }

        stopRequested = true;
        //System.out.println(stopRequested);
    }
}

 

이 코드는 **멀티스레딩**과 **변수 가시성** 문제를 다루고 있는 예시입니다. 특히, **volatile 키워드**를 사용하지 않으면 스레드 간의 변수 변경 사항이 공유되지 않아서 문제가 발생할 수 있음을 보여줍니다.

### 코드 설명

1. **stopRequested 변수**:
   ```java
   private static boolean stopRequested = false;
   ```
   - `stopRequested`는 **스레드 중지 신호** 역할을 하는 `boolean` 변수입니다.
   - 이 변수는 `main` 스레드에서 `true`로 변경되면 **백그라운드 스레드**가 멈추도록 설계되어 있습니다.
   - 하지만 이 변수가 **volatile**로 선언되지 않았기 때문에, 스레드 간의 변수 변경 사항이 제대로 전달되지 않는 문제가 있습니다.

2. **백그라운드 스레드**:
   ```java
   Thread backgroundThread = new Thread(() -> {
       int i = 0;
       while (!stopRequested) {
            i++;
       }
   });
   ```
   - 새로운 **백그라운드 스레드**가 생성되어, `while (!stopRequested)` 조건을 만족하는 한 무한 루프를 실행합니다.
   - 루프가 `stopRequested`가 `true`로 설정되면 종료되도록 설계되었습니다.
   - 그러나 `stopRequested`가 다른 스레드에서 변경되었을 때, **백그라운드 스레드가 이를 알지 못하는 문제**가 발생합니다. 이는 메모리 가시성 문제로, 한 스레드에서의 변수 변경이 다른 스레드에서는 즉시 반영되지 않을 수 있기 때문입니다.

3. **메인 스레드에서의 변수 변경**:
   ```java
   try {
       TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
       System.out.println("Thread was interrupted.");
   }

   stopRequested = true;
   ```
   - 메인 스레드는 1초 동안 **sleep**하고 나서, `stopRequested` 변수를 `true`로 설정하여 **백그라운드 스레드**를 중지하려 합니다.
   - 문제는 `stopRequested` 변수의 변경 사항이 백그라운드 스레드에 **즉시 반영되지 않아서**, **백그라운드 스레드가 계속 루프를 돌게 되는 문제**가 발생합니다.

### 문제점: **변수 가시성 문제**
- `stopRequested` 변수가 **volatile**로 선언되지 않았기 때문에, 메인 스레드에서 이 변수를 `true`로 변경해도 **백그라운드 스레드에서 변경 사항을 즉시 인식하지 못합니다**.
- 이것이 **메모리 가시성 문제**로, 각 스레드가 메모리에서 직접 변수 값을 읽는 것이 아니라 **자신만의 캐시**에 있는 값을 사용할 수 있기 때문에 발생합니다.
- 따라서, 백그라운드 스레드는 `stopRequested = true`가 되었음에도 여전히 `false`로 인식하고 계속 루프를 돌게 됩니다.

### 해결 방법: **volatile 키워드**
- 이 문제는 `stopRequested` 변수를 **volatile**로 선언하여 해결할 수 있습니다. `volatile`로 선언된 변수는 **모든 스레드가 해당 변수의 값을 메인 메모리에서 바로 읽고 쓰게** 합니다.
  
  수정된 코드는 다음과 같습니다:

  ```java
  private static volatile boolean stopRequested = false;
  ```

  이 변경 사항으로 인해, 메인 스레드가 `stopRequested = true`로 변경하면 **즉시** 백그라운드 스레드에서도 이 변경 사항을 인식하고 루프를 종료하게 됩니다.

### 요약:
- 이 코드는 멀티스레딩 환경에서 **변수 가시성** 문제가 발생하는 상황을 보여줍니다.
- `stopRequested` 변수가 **volatile**로 선언되지 않아, **스레드 간의 변경 사항이 즉시 반영되지 않는** 문제가 있습니다.
- 이 문제를 해결하기 위해 **volatile 키워드**를 사용하여 모든 스레드가 해당 변수를 **메인 메모리**에서 바로 읽고 쓸 수 있도록 해야 합니다.

 

 

 

 

package multithreading.synchronization.volatileKeyword;

import java.util.concurrent.TimeUnit;

/** Example from Effective Java book.
 * This version of the program stops because stopRequested is volatile
 * (all reads are from the main memory, all writes are to the main memory). */
public class StopThreadVolatile {
    private static volatile boolean stopRequested;

    public static void main (String[] args) {
        // run
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                i++;
            } // while
        }); //Thread

        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        }
        catch (InterruptedException e) {
            System.out.println("Thread was interrupted.");
        }

        stopRequested = true;
    }
}

Volatile

`volatile` 키워드를 추가함으로써 이 프로그램이 **정상적으로 종료**되는 이유는 **메모리 가시성 문제**가 해결되었기 때문입니다. 아래에서 이 현상을 더 자세히 설명하겠습니다.

### 문제 원인 (volatile 사용 전):
- 기본적으로, **멀티스레딩** 환경에서 각 스레드는 **자신의 캐시 메모리**를 사용합니다. 
- `stopRequested` 변수가 `volatile`이 아닌 경우, 한 스레드에서 이 변수를 수정해도 다른 스레드가 이를 즉시 **메인 메모리**에서 읽지 못하고, 각 스레드의 **캐시된 값**을 사용하게 됩니다.
- 그래서 `main` 스레드에서 `stopRequested = true`로 설정해도 **백그라운드 스레드**는 이 변경 사항을 알지 못하고 계속 무한 루프를 돌게 됩니다.

### volatile 키워드란?
- `volatile` 키워드는 **변수에 대한 읽기/쓰기 작업이 항상 메인 메모리에서 직접적으로 이루어지도록 보장**합니다.
- `volatile`로 선언된 변수는 **캐시되지 않고**, 모든 스레드가 **메인 메모리**에서 해당 변수의 값을 읽고 쓰기 때문에 **스레드 간의 가시성 문제**가 해결됩니다.
- 즉, 한 스레드에서 `volatile` 변수를 변경하면 다른 스레드는 그 값을 **즉시 인식**하게 됩니다.

### 프로그램 흐름 설명 (volatile 적용 후):
1. **`volatile stopRequested` 선언**:
   - 이 변수는 메인 메모리에서 **직접 접근**되며, 모든 스레드가 이 변수를 메모리에서 바로 읽고 쓰게 됩니다.

2. **메인 스레드**:
   ```java
   stopRequested = true;
   ```
   - 1초 후, `stopRequested` 변수를 `true`로 변경합니다.
   - 이때, 이 변경 사항은 즉시 **메인 메모리에 기록**됩니다.

3. **백그라운드 스레드**:
   - 백그라운드 스레드는 무한 루프를 돌며 `while (!stopRequested)` 조건을 확인합니다.
   - `stopRequested`가 **volatile**로 선언되었기 때문에, **메인 메모리에서 직접 값을 읽어오게 됩니다**.
   - 메인 스레드가 `stopRequested = true`로 변경하면, **백그라운드 스레드도 즉시 이 변경 사항을 인식**하고 `while` 루프가 종료됩니다.

### 왜 volatile이 문제를 해결하는가?
- **메모리 가시성**: `volatile`로 선언된 변수는 모든 스레드에서 **항상 메인 메모리**에서 값을 읽습니다. 즉, 스레드 간에 **변수의 최신 상태**가 항상 공유됩니다.
- **캐싱 문제 해결**: `volatile` 변수를 사용함으로써 각 스레드가 **자신만의 캐시된 값을 사용하는 문제**가 사라지고, 모든 스레드가 **최신 값**을 참조하게 됩니다.
  
### volatile로 인한 동작:
1. `stopRequested`가 `true`로 변경되면, **즉시** 모든 스레드에서 이 변경 사항을 볼 수 있습니다.
2. **백그라운드 스레드**는 `stopRequested`가 `true`가 되는 즉시 `while` 루프를 빠져나와 종료됩니다.

### 주의 사항:
- `volatile` 키워드는 **변수의 가시성 문제**를 해결할 수 있지만, **원자성(atomicity)**을 보장하지는 않습니다. 즉, 복잡한 동시성 문제가 있는 경우에는 여전히 `synchronized`나 다른 동기화 메커니즘이 필요할 수 있습니다.

### 요약:
- `volatile` 키워드는 **스레드 간 변수의 최신 상태를 공유**할 수 있게 해주며, 이를 통해 메모리 가시성 문제를 해결합니다.
- 이 코드는 `volatile` 키워드를 사용하여 `stopRequested` 변수가 메인 메모리에 즉시 기록되고, 다른 스레드에서도 그 값을 즉시 인식하게 만듭니다.
- 따라서 백그라운드 스레드가 **정상적으로 종료**됩니다.

 

 

==============================================================

 

 

여기서 멈추는 이유는?

package multithreading.synchronization.volatileKeyword;

import java.util.concurrent.TimeUnit;

/** Example from Effective Java book. The program does not stop.
 * The issue is with visibility. */
public class StopThread {
    private static boolean stopRequested = false;

    public static void main (String[] args) {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                 i++;
                System.out.println(i); 
                //System.out.println(stopRequested);
            } // while
            //System.out.println(stopRequested);
        }); //Thread

        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        }
        catch (InterruptedException e) {
            System.out.println("Thread was interrupted.");
        }

        stopRequested = true;
        //System.out.println(stopRequested);
    }
}

 

이 코드가 멈추는 이유는 **메모리 가시성 문제** 때문입니다. `stopRequested` 변수가 **volatile**로 선언되지 않았기 때문에, **스레드 간에 변수 값의 변경 사항이 제대로 공유되지 않기** 때문입니다.

### 자세한 설명:

1. **메모리 가시성 문제**:
   - **멀티스레딩 환경**에서 각 스레드는 자신만의 **캐시**를 사용할 수 있습니다. 즉, 스레드는 변수의 값을 메인 메모리에서 읽는 것이 아니라 **자신만의 캐시에 저장된 값을 사용할 수 있습니다**.
   - 이 코드에서는 `stopRequested` 변수가 **volatile**로 선언되지 않았기 때문에, **메인 스레드**에서 `stopRequested = true`로 변경한 것이 **백그라운드 스레드**에 즉시 반영되지 않습니다.
   - 백그라운드 스레드는 `stopRequested`가 여전히 `false`인 것으로 인식하고 무한 루프를 돌면서 계속 실행되게 됩니다.

2. **백그라운드 스레드**:
   - 백그라운드 스레드는 `while (!stopRequested)`라는 조건을 만족하는 한 계속 실행됩니다.
   - `stopRequested`가 **volatile**로 선언되지 않았기 때문에, 백그라운드 스레드는 이 변수의 변경 사항을 인식하지 못하고 **계속해서 `false`인 상태로 루프**를 돌게 됩니다.

3. **해결 방법**:
   - 이 문제를 해결하려면, **변수 가시성**을 보장하기 위해 `stopRequested` 변수를 **`volatile`**로 선언해야 합니다. 이렇게 하면 **메인 스레드**에서 `stopRequested`를 `true`로 변경했을 때, **백그라운드 스레드**도 즉시 이 변경 사항을 인식하게 됩니다.
   
   수정된 코드는 다음과 같습니다:

   ```java
   private static volatile boolean stopRequested = false;
   ```

   - 이렇게 `volatile`을 추가하면 **메모리 가시성 문제**가 해결되고, 백그라운드 스레드는 메인 스레드에서 변경한 `stopRequested` 값을 즉시 읽어올 수 있어 **정상적으로 루프를 종료**할 수 있습니다.

### 요약:
- 이 코드가 멈추는 이유는 **메모리 가시성 문제** 때문입니다. `stopRequested` 변수가 **volatile**로 선언되지 않았기 때문에, **변경 사항이 다른 스레드에 즉시 반영되지 않아** 백그라운드 스레드가 무한 루프를 계속 돌게 됩니다.
- 이 문제를 해결하려면 `stopRequested`를 **volatile**로 선언하여, 스레드 간의 메모리 가시성을 보장해야 합니다.

 

==============================================================

 

 

 

==============================================================

 

 

이 설명은 **Java의 `volatile` 변수**와 **Happens-Before** 관계에 대한 내용입니다. `volatile` 변수는 **스레드 간의 메모리 가시성**을 보장하며, 동시에 **명령어 재정렬**이 일어나지 않도록 강제합니다. 여기서 제시된 예시는 **`volatile` 변수**가 스레드 간 동기화를 어떻게 보장하는지 설명하고 있습니다.

### 1. **Happens-Before 규칙**:
   - **Happens-Before**는 Java 메모리 모델에서 중요한 개념으로, 두 작업 간의 **실행 순서를 보장**하는 것을 의미합니다. 
   - 한 작업이 다른 작업보다 **먼저 실행됨을 보장**하는 관계입니다.
   - `volatile` 변수는 **Happens-Before 관계**를 강제로 만들며, **읽기/쓰기 작업**이 **명령어 재정렬 없이** 안전하게 실행되도록 합니다.

### 2. **명령어 재정렬 방지**:
   - **`volatile` 변수에 대한 읽기/쓰기 작업은 재정렬되지 않습니다**. 즉, Java 컴파일러나 CPU가 `volatile` 변수 관련 명령어를 **앞뒤로 이동**시켜 최적화하지 않도록 강제됩니다.
   - 이를 통해 `volatile` 변수를 쓰기 전의 명령어는 **모두 그 이전에** 실행되고, `volatile` 변수를 읽은 후의 명령어는 **모두 그 이후에** 실행됩니다.
   - **Happens-Before 보장**: `volatile` 변수를 사용하면, 그 전후로 실행되는 명령어의 순서가 **엄격하게 보장**됩니다.

### 3. **예시 설명**:
   이 예시는 `volatile` 변수가 다른 변수들과의 관계에서 **순서 보장**을 어떻게 하는지를 보여줍니다.

   ```java
   sharedObject.nonVolatile1 = 123;
   sharedObject.nonVolatile2 = 456;
   sharedObject.volatileVar = true;
   int someValue1 = sharedObject.nonVolatile3;
   int someValue2 = sharedObject.nonVolatile4;
   ```

   - **첫 번째 줄과 두 번째 줄 (파란색 부분)**:
     ```java
     sharedObject.nonVolatile1 = 123;
     sharedObject.nonVolatile2 = 456;
     ```
     - **설명**: 이 두 줄은 `volatileVar`에 값을 **쓰기 전**에 실행됩니다. 즉, `volatileVar = true;` 명령어가 실행되기 전에 반드시 실행됨을 보장합니다. 이는 `volatile` 변수로 인해 이 이전의 명령어들이 재정렬되지 않기 때문입니다.
     - 즉, **`volatileVar = true;`가 실행되기 전에** `nonVolatile1`과 `nonVolatile2`의 값이 설정되었음을 보장합니다.
   
   - **세 번째 줄 (volatile 쓰기)**:
     ```java
     sharedObject.volatileVar = true;
     ```
     - **설명**: 이 줄은 `volatile` 변수를 **쓰기** 작업입니다. 이 작업은 앞뒤로 명령어가 재정렬되지 않도록 보장되며, 다른 스레드에서는 이 `volatileVar`가 `true`로 설정되었다는 사실을 확실히 인식할 수 있습니다.

   - **네 번째와 다섯 번째 줄 (초록색 부분)**:
     ```java
     int someValue1 = sharedObject.nonVolatile3;
     int someValue2 = sharedObject.nonVolatile4;
     ```
     - **설명**: 이 두 줄은 `volatileVar`에 값을 **쓴 후**에 실행됩니다. 즉, `volatileVar`가 `true`로 설정된 이후에만 이 두 줄이 실행됨을 보장합니다.
     - 이는 `volatile` 변수의 **Happens-Before 규칙**에 의해, `volatileVar = true;` 이후에 나오는 명령어는 반드시 **그 이후에 실행됨**을 보장받기 때문입니다.

### 4. **핵심 요약**:
- `volatile` 변수는 스레드 간의 **메모리 가시성**을 보장하며, 변수의 **읽기/쓰기** 작업이 다른 스레드에서 즉시 반영될 수 있도록 합니다.
- **명령어 재정렬**을 방지하여, `volatile` 변수를 기준으로 그 이전의 명령어는 먼저 실행되고, 그 이후의 명령어는 나중에 실행됩니다.
- 이로 인해 **`volatile` 변수**를 사용하면 **Happens-Before 관계**가 생기며, 그 변수의 쓰기 작업과 읽기 작업 간의 **순서 보장**이 이루어집니다.

 

==============================================================

 

Is Volatile Enough?

 

Atomacity

 

 

 

==============================================================

 

 

 

Volatile이 다 해결 못함. 그거를 Synchronize로 해결할거임

 

`volatile` 키워드는 자바에서 **가시성**을 보장하지만, **원자성(atomicity)**을 보장하지는 않습니다. 이는 여러 스레드가 공유하는 변수를 수정할 때, 그 변경 사항이 즉시 다른 스레드에 보이도록 보장하지만, **여러 스레드가 동시에 해당 변수를 수정하는 상황**을 막지는 못합니다. 복잡한 연산에서는 `volatile`만으로는 충분하지 않으며, 원자성을 보장하기 위한 추가적인 조치가 필요합니다. 여기서 **원자성**과 `volatile`의 관계를 자세히 살펴보겠습니다.

### 1. **Volatile은 가시성을 보장하지만 원자성을 보장하지 않음**:
   - **가시성**: `volatile` 키워드는 한 스레드가 `volatile` 변수에 값을 쓰면, 다른 스레드가 그 변경된 값을 즉시 확인할 수 있음을 보장합니다. 이는 **가시성 문제**를 해결해 줍니다.
   - **원자성**: 하지만 **원자성**은 여러 단계의 작업이 **분리되지 않고 하나의 작업처럼 처리**되는 것을 의미합니다. `volatile`만으로는 여러 단계로 이루어진 연산이 동시에 수행되는 것을 막지 못합니다.

   예를 들어, 다음과 같은 `volatile` 변수를 사용하는 증감 연산은 **원자적이지 않습니다**:
   ```java
   sharedCounter++;  // 실제로는: sharedCounter = sharedCounter + 1;
   ```
   `volatile`은 `sharedCounter`의 최신 값을 다른 스레드가 바로 확인할 수 있게 해주지만, 이 증감 연산은 세 단계로 나뉩니다:
   1. `sharedCounter`의 현재 값을 읽음.
   2. 1을 더함.
   3. 새로운 값을 `sharedCounter`에 저장함.

   이 과정이 **원자적이지 않아서**, 두 개의 스레드가 동시에 이 작업을 수행할 경우 **경쟁 상태(race condition)**가 발생할 수 있습니다:
   - **스레드 A**가 `sharedCounter`의 값을 읽고 1을 더한 후, 값을 저장하기 전에 **스레드 B**가 동일한 값을 읽어 1을 더하게 됩니다.
   - 이 경우, 두 스레드 모두 값이 1 증가한 동일한 값을 저장하게 되어, **의도한 2번의 증가가 아닌 1번만 증가하는** 상황이 발생합니다.

   따라서, **volatile은 이런 경쟁 상태를 막지 못합니다**.

### 2. **원자성을 보장하기 위해 필요한 것**:
   원자성을 보장하기 위해서는 **synchronized** 같은 동기화 메커니즘을 사용하거나, **`AtomicInteger`**와 같은 **원자적 클래스**를 사용하는 것이 필요합니다.

   - **synchronized 키워드**: 이 키워드를 사용하면 **한 번에 하나의 스레드만 코드 블록을 실행**하도록 보장할 수 있습니다.
   - **`AtomicInteger`**: 이 클래스는 **원자적 연산**을 제공하여, 스레드 간 경쟁 상태 없이 안전하게 값을 변경할 수 있습니다.

   예를 들어, `AtomicInteger`를 사용하는 방법은 다음과 같습니다:
   ```java
   AtomicInteger sharedCounter = new AtomicInteger(0);
   sharedCounter.incrementAndGet(); // 이 연산은 원자적입니다.
   ```

   이를 통해 **증감 연산이 원자적으로 실행**되며, 동시성 문제를 방지할 수 있습니다.

### 3. **복잡한 연산에서는 Volatile만으로 충분하지 않음**:
   더 복잡한 예로, "검사 후 실행" 연산을 생각해 봅시다:
   ```java
   if (sharedFlag == false) {
       sharedFlag = true;
       // 작업 수행...
   }
   ```
   이 경우, `sharedFlag`가 `volatile`로 선언되었다 하더라도, **검사 후 실행(check-then-act)** 연산은 **원자적이지 않기** 때문에 두 개의 스레드가 동시에 `sharedFlag`가 `false`라고 판단하고 값을 변경할 수 있습니다.

   이러한 경우에는 **synchronized** 블록이나 **원자적 클래스**를 사용하여, **전체 연산이 원자적으로 수행되도록** 만들어야 합니다.

### 4. **CAS(Compare-And-Swap)와 원자성**:
   - **`AtomicInteger`**와 같은 클래스는 **CAS(비교 후 교환)** 메커니즘을 사용하여 원자성을 보장합니다.
   - CAS는 값이 기대한 값과 일치하는지 확인하고, 일치하면 그 값을 새로운 값으로 **원자적으로** 교환합니다.
   - 이를 통해 **동기화 없이도** 빠르고 안전하게 원자적 연산을 수행할 수 있습니다.

### 5. **요약**:
   - **volatile**은 스레드 간의 **가시성**을 보장하지만, **원자성**을 보장하지 않습니다.
   - 복잡한 연산(예: 증가 연산, 검사 후 실행)에서는 `volatile`만으로 충분하지 않으며, **synchronized** 블록이나 **원자적 클래스**를 사용해야 합니다.
   - **원자성**을 보장하기 위해서는 CAS 같은 메커니즘을 사용하여 **경쟁 상태**를 방지할 수 있습니다.

따라서, `volatile`은 **가시성**을 보장하지만, **원자성**을 보장하지 않기 때문에 멀티스레드 환경에서의 안전한 작업을 위해서는 추가적인 동기화가 필요합니다.

 

 

==============================================================

 

 

 

 

package multithreading.synchronization;

public class NoSynchronization {
    private volatile int x = 0;

    private class AdditionWorker implements Runnable {
       public void run() {
          for (int i = 0; i < 1000; i++)
             x++;
       }

    }

    private class SubtractionWorker implements Runnable {
       public void run() {
          for (int i = 0; i < 1000; i++)
             x--;
       }
    }

    public void createThreads() throws InterruptedException {
       Thread t1 = new Thread(new AdditionWorker());
       Thread t2 = new Thread(new SubtractionWorker());
       t1.start();
       t2.start();
       t1.join();
       t2.join();
       //System.out.println(x);
       if (x != 0)
          System.out.print(x + " ");
       // System.out.println("Done");
    }

    public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 1000; i++) {
          NoSynchronization ns = new NoSynchronization();
          ns.createThreads();
       }
    }

}

 

 

이 코드에서 `if (x != 0)` 구문은 **멀티스레딩에 의한 경쟁 상태(race condition)**가 발생했는지 여부를 확인하는 역할을 합니다.

### 설명:
1. **`volatile` 키워드**:
   - `x`는 `volatile`로 선언되어 있어서, 각 스레드는 변수 `x`의 최신 값을 항상 읽고 쓰게 됩니다. 하지만 `volatile`은 **가시성**을 보장할 뿐, **원자성**은 보장하지 않습니다.
   - 즉, `x++` 또는 `x--` 연산이 여러 단계(읽기, 연산, 쓰기)로 나뉘기 때문에 **경쟁 상태**가 발생할 수 있습니다. 여러 스레드가 동시에 `x`에 접근하면, 값이 제대로 업데이트되지 않을 가능성이 있습니다.

2. **경쟁 상태(Race Condition)**:
   - `AdditionWorker` 스레드는 `x`를 1씩 증가시키고, `SubtractionWorker` 스레드는 `x`를 1씩 감소시킵니다.
   - 이 두 스레드가 동시에 실행되면 `x++` 또는 `x--`가 동시에 발생할 수 있습니다. 이 경우, `x`의 값을 제대로 읽거나 쓰지 못할 가능성이 높습니다.
   - 예를 들어, 스레드 A가 `x`의 값을 읽고 1을 더하는 중간에, 스레드 B가 값을 읽고 1을 빼버리면, 값이 제대로 증가하거나 감소하지 않을 수 있습니다.

3. **`if (x != 0)`의 역할**:
   - 프로그램이 정상적으로 동작한다면, 두 스레드가 각각 `x`를 1000번씩 증가시키고 감소시키므로, **이론적으로 `x`는 0**이어야 합니다.
   - 하지만, **경쟁 상태**가 발생하면 `x`의 값이 0이 아닌 다른 값으로 남아 있을 수 있습니다.
   - `if (x != 0)` 구문은 이런 경우를 찾아내기 위해 존재하며, **`x`가 0이 아닌 경우에만 출력**하는 역할을 합니다.
   - 즉, 이 조건문은 경쟁 상태로 인해 발생한 **데이터 불일치**를 확인하는 역할을 합니다.

### 예시 시나리오 (경쟁 상태가 발생하는 경우):
1. 스레드 A가 `x++`를 수행하기 위해 `x`의 현재 값을 읽습니다 (예: `x = 0`).
2. 동시에, 스레드 B가 `x--`를 수행하기 위해 같은 값을 읽습니다 (`x = 0`).
3. 스레드 A는 `x`에 1을 더해 `1`을 저장하려고 하지만, 그 전에 스레드 B가 `x`에서 1을 빼서 `-1`로 만듭니다.
4. 최종적으로 `x`는 `-1`이 될 수도 있고, **값이 정상적으로 증가/감소되지 않았습니다.**

따라서, **`if (x != 0)`** 구문은 멀티스레딩 문제로 인해 `x`의 값이 예상과 다를 경우를 포착하는 도구로 작동합니다.

 

 

==============================================================

 

 

package multithreading.synchronization;

/**  Based on NoSynchronization example */
public class WithSynchronization {
    private int x = 0;
    private Object lock  = new Object(); // belongs to the outer class, will be shared by AdditionTask and SubtractionTask

    private class AdditionTask implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                synchronized (lock) {
                    x++;
                }
            }
        }
    }

    private class SubtractionTask implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                synchronized(lock) {
                    x--;
                }

            }
        }
    }

    public void createThreads() throws InterruptedException {
        Thread t1 = new Thread(new AdditionTask());
        Thread t2 = new Thread(new SubtractionTask());
        t1.start();
        t2.start();
        // wait for threads 1 and 2 to  finish
        t1.join();
        t2.join();

        // Note: we are reading x below, but no need to synchronize here since t1 and t2 finished
        if (x != 0) // will only print something if x i not 0
            System.out.print(x + " ");
        // System.out.println("Done");
    }

    public static void main(String[] args)  {
        WithSynchronization ns = new WithSynchronization();
        try {
            for (int i = 0; i < 10000; i++) {
                ns.createThreads();
            }
        }
        catch  (InterruptedException e) {
            System.out.println("Thread got interrupted: " + e);
        }
        System.out.println("Done");
    }

}

 

이 코드와 이전 `NoSynchronization` 예제의 차이점은 **동기화(synchronization)**를 사용하여 여러 스레드가 **동시에 공유 변수에 접근하는 문제**를 해결한 것입니다.

### 주요 차이점:
1. **동기화된 블록(synchronized block)**:
   - **`synchronized (lock)`**: 이 코드는 `lock` 객체를 기준으로 동기화된 블록을 만들어, 한 번에 **오직 하나의 스레드만** `x`를 읽거나 수정할 수 있도록 보장합니다.
   - 즉, **경쟁 상태(race condition)**를 방지하기 위해, 각 스레드가 `x++` 또는 `x--` 연산을 수행할 때 다른 스레드가 같은 연산을 동시에 수행하지 않도록 막습니다.
   
2. **동기화의 효과**:
   - **NoSynchronization** 예제에서는 `x++`와 `x--` 연산이 원자적이지 않아서, 두 스레드가 동시에 `x` 값을 읽고 쓰는 과정에서 충돌(경쟁 상태)이 발생할 수 있었습니다.
   - 반면, **WithSynchronization**에서는 `synchronized` 블록을 통해 하나의 스레드가 `x`에 접근할 때 다른 스레드는 대기하게 됩니다. 따라서 한 번에 하나의 스레드만 `x` 값을 읽고 수정할 수 있어, 값이 일관되게 유지됩니다.

3. **`volatile` 대신 `synchronized`**:
   - 이전 예제에서는 `volatile` 키워드를 사용했지만, 그 자체로는 원자성을 보장하지 못했습니다.
   - 이번 예제에서는 `volatile` 대신 **`synchronized` 블록**을 사용하여 **원자적 연산(atomic operation)**을 보장합니다. 즉, `x++`와 `x--` 연산이 더 이상 경쟁 상태를 일으키지 않고, 각 스레드가 안전하게 연산을 수행할 수 있습니다.

4. **동기화가 적용된 부분**:
   - `x++`와 `x--` 연산이 동기화된 블록 안에서 수행되기 때문에, 여러 스레드가 같은 변수를 동시에 읽고 쓰는 문제가 해결됩니다. 
   - 즉, `AdditionTask`와 `SubtractionTask`는 더 이상 경쟁 상태를 일으키지 않으며, `x`의 값은 항상 정확하게 계산됩니다.

### 요약:
- **NoSynchronization**: 두 스레드가 동시에 `x++` 또는 `x--`를 실행할 때 충돌이 발생하여 `x`의 값이 불일치할 수 있었습니다.
- **WithSynchronization**: `synchronized` 블록을 사용하여 한 번에 하나의 스레드만 `x`에 접근할 수 있게 하여 경쟁 상태를 방지하고, `x`의 값을 정확하게 유지합니다.

따라서, **WithSynchronization**에서는 동기화를 통해 멀티스레드 환경에서 안전하게 공유 변수를 사용할 수 있습니다.

 

 

==============================================================

 

package multithreading.synchronization;

/**
 * @author Prof. Engle 
 *         This class demonstrates the impact different types of
 *         lock objects have on whether multiple threads may run synchronized
 *         blocks of code simultaneously. OuterClass is courtesy of Prof. Engle.
 */
public class LockDemo {

    // Two worker threads:
    private final Thread worker1;
    private final Thread worker2;

    // Different types of locks
    private final static Object staticLock = new Object();
    private final Object outerLock;
    private final Object multiLock;

    public LockDemo(String name) {
       outerLock = new Object();
       multiLock = new Object();

       /*
        * Comment in/out different examples and see how the behavior changes.
        */

       // TT TT FF FF
       // Each worker uses a different lock
       // So they do not need to compete for the locks - each of them can obtain a lock at the same time
       worker1 = new Worker(outerLock);
       worker2 = new Worker(multiLock);

       // TT FF TT FF
       // Both workers 1 and 2 of this instance of LockDemo get the same lock
       // So workers need to compete for the lock
       /*worker1 = new Worker(outerLock);
       worker2 = new Worker(outerLock);*/
       
       // TT FF TT FF
       // Same case as before. Workers 1 and 2 of this instance of LockDemo
       // compete for one lock, "this"
       /*worker1 = new Worker(this);
       worker2 = new Worker(this);
       */
       // TF TF TF TF
       // There is only one lock for all workers for all instances of LockDemo
       // So they all need to compete for this lock
       //worker1 = new Worker(staticLock);
       //worker2 = new Worker(staticLock);


       // TT TT FF FF
       // Each worker is initialized with a brand new lock
       // So there is no competition between threads, they can each grab their own lock
       //worker1 = new Worker(new Object());
       //worker2 = new Worker(new Object());

       worker1.setName(name + "1");
       worker2.setName(name + "2");

       worker1.start();
       worker2.start();
    }

    public void joinAll() throws InterruptedException {
       worker1.join();
       worker2.join();
    }

    private class Worker extends Thread {

       private final Object lock;

       public Worker(Object lock) {
          this.lock = lock;
          // this.lock = new Object();
       }

       @Override
       public void run() {
          synchronized (lock) {
             System.out.println(this.getName() + ": " + Thread.holdsLock(lock));

             try {
                // The thread will keep its lock while sleeping!
                Thread.sleep(1000);
             } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
             }
          }

          //System.out.println(this.getName() + ": " + Thread.holdsLock(lock));

       }
    }

    public static void main(String[] args) throws InterruptedException {
       LockDemo demo1 = new LockDemo("A");
       LockDemo demo2 = new LockDemo("B");

       demo1.joinAll();
       demo2.joinAll();

       /*
        * We have the following threads TRYING to run at the same time:
        *
        * +---LockDemo A---+  +---LockDemo B---+
        * | +-A1-+ +-A2-+  |  | +-B1-+ +-B2-+
        *
        * Whether A1, A2, B1, B2 are able to run simultaneously (e.g. threads
        * are able to enter the "locked rooms" setup by each thread) depends on
        * the type of lock used.
        */
    }
}

 

 

이 코드는 **멀티스레딩** 환경에서 **동기화된 블록**을 어떻게 다루는지에 대해 설명하는 예제입니다. 특히, **다양한 종류의 락(lock) 객체**가 여러 스레드의 실행에 어떤 영향을 미치는지를 보여줍니다. 각 스레드는 특정한 락을 획득해야만 동기화된 블록에 접근할 수 있으며, 서로 다른 종류의 락을 사용할 때 스레드가 동시에 실행될 수 있는지 여부가 달라집니다.

### 주요 구성 요소 설명:

1. **락 객체들 (Locks)**:
   - **`staticLock`**: **static**으로 선언된 락이므로, 모든 `LockDemo` 객체에서 공유되는 전역 락입니다.
   - **`outerLock`**: 각 `LockDemo` 인스턴스에 대해 서로 다른 객체입니다.
   - **`multiLock`**: `outerLock`과 유사하게, 각 인스턴스마다 다르게 사용되는 객체입니다.

2. **스레드 구성**:
   - 이 클래스는 두 개의 스레드 (`worker1`, `worker2`)를 생성합니다. 각 스레드는 `Worker` 클래스의 객체로, 동기화된 블록 안에서 특정 작업을 수행합니다.
   - `Worker` 클래스의 **`run()`** 메서드는 스레드가 수행할 작업을 정의합니다. 이때, 동기화된 블록을 사용하여 **동시성 문제를 방지**합니다.

### 동기화된 블록:
- **`synchronized (lock)`**: 이 부분에서 스레드는 특정 `lock` 객체에 대해 락을 요청하고, 락을 획득한 스레드만이 해당 블록을 실행할 수 있습니다. 다른 스레드는 락이 해제될 때까지 기다립니다.

### 코드에서 테스트할 수 있는 다양한 락 구성:
코드에는 주석 처리된 여러 테스트 케이스가 있습니다. 이들은 서로 다른 락을 사용하여 스레드가 어떻게 동작하는지를 실험할 수 있도록 구성되어 있습니다.

1. **각 스레드가 다른 락을 사용하는 경우**:
   ```java
   worker1 = new Worker(outerLock);
   worker2 = new Worker(multiLock);
   ```
   - `worker1`과 `worker2`가 각각 다른 락 (`outerLock`, `multiLock`)을 사용합니다.
   - 두 스레드는 서로 다른 락을 사용하기 때문에, **동시에 실행**될 수 있습니다. **동기화에 방해받지 않습니다.**

2. **두 스레드가 같은 락을 사용하는 경우**:
   ```java
   worker1 = new Worker(outerLock);
   worker2 = new Worker(outerLock);
   ```
   - `worker1`과 `worker2`가 같은 락 (`outerLock`)을 사용합니다.
   - 이 경우, **동시에 실행되지 않고** **락을 차례로 획득**하여 동기화된 블록을 실행합니다. 하나의 스레드가 락을 해제할 때까지 다른 스레드는 대기합니다.

3. **`this` 객체를 락으로 사용하는 경우**:
   ```java
   worker1 = new Worker(this);
   worker2 = new Worker(this);
   ```
   - 두 스레드가 동일한 `LockDemo` 객체 자체를 락으로 사용하여 동기화됩니다. **동시에 실행되지 않고, 락을 차례로 획득**합니다.

4. **정적(static) 락을 사용하는 경우**:
   ```java
   worker1 = new Worker(staticLock);
   worker2 = new Worker(staticLock);
   ```
   - `staticLock`은 모든 `LockDemo` 인스턴스에 대해 공유되는 전역 락입니다.
   - 두 스레드는 전역 락을 사용하여 **모든 `LockDemo` 인스턴스에 대해 동기화**됩니다. 즉, **A1, A2, B1, B2** 스레드 모두 이 `staticLock`을 사용하게 되어, **한 번에 하나의 스레드만 실행**됩니다.

5. **각 스레드가 새로운 락을 사용하는 경우**:
   ```java
   worker1 = new Worker(new Object());
   worker2 = new Worker(new Object());
   ```
   - 각 스레드는 새로운 락을 사용하므로, **동시에 실행**될 수 있습니다. **서로 간섭하지 않습니다.**

### 코드 흐름:
1. `LockDemo` 객체 두 개 (`demo1`, `demo2`)가 생성됩니다.
2. 각 `LockDemo` 객체는 두 개의 스레드 (`worker1`, `worker2`)를 생성하여 실행합니다.
3. 각 스레드는 동기화된 블록 안에서 1초 동안 잠을 잡니다 (`Thread.sleep(1000)`).
4. 동기화된 블록을 사용하는 방식에 따라 스레드들이 **동시에 실행**될 수도 있고, **순차적으로 실행**될 수도 있습니다.

### 실험할 수 있는 사항:
- 주석 처리된 부분을 켜고 끄면서 **어떤 스레드가 언제 실행되는지**를 확인할 수 있습니다.
- `staticLock`을 사용할 때, 모든 `LockDemo` 객체의 스레드들이 전역 락을 공유하기 때문에, 한 번에 하나의 스레드만 동작합니다.
- 반면, 각 스레드가 다른 락을 사용할 때는 **동시에 실행**됩니다.

### 결론:
이 코드는 **동기화된 블록**에서 락을 어떻게 설정하는지에 따라 스레드의 동작이 어떻게 달라지는지를 보여줍니다. 락이 동일한 경우에는 **경쟁**이 발생하여 스레드가 순차적으로 실행되지만, **서로 다른 락**을 사용하는 경우에는 **동시에 실행**될 수 있습니다.

 

==============================================================

 

 

이 코드에서 `LockDemo`는 두 개의 스레드 (`worker1`, `worker2`)가 다른 종류의 락을 사용할 수 있도록 설계되어 있습니다. 각 스레드는 특정 락을 획득하고 그 락을 사용하여 동기화된 블록에서 코드를 실행합니다. **동시에 실행되거나(concurrently)**, **순차적으로 실행되거나(sequentially)**, **락을 공유하거나(share lock)** 하는 상황은 스레드들이 어떤 락을 사용하는지에 따라 달라집니다.

### 1) **Run Concurrently (동시에 실행되는 경우)**
동시에 실행된다는 것은 두 스레드가 **서로 다른 락을 사용**하기 때문에 **동시에 실행**될 수 있다는 것을 의미합니다. 각 스레드는 자신만의 락을 획득하여 다른 스레드와 경합 없이 코드를 실행할 수 있습니다.

#### 코드 예시:
```java
worker1 = new Worker(outerLock);
worker2 = new Worker(multiLock);
```
여기서 `worker1`과 `worker2`는 **서로 다른 락(outerLock, multiLock)**을 사용합니다. 이 경우, 두 스레드는 동시에 실행될 수 있습니다. 각 스레드는 다른 락을 사용하기 때문에, 두 스레드는 서로 간섭 없이 동시 실행이 가능합니다.

### 2) **Run Sequentially (순차적으로 실행되는 경우)**
순차적으로 실행된다는 것은 두 스레드가 **같은 락을 공유**하고 있어서 **하나의 스레드가 락을 획득한 상태에서는 다른 스레드가 대기**해야 한다는 뜻입니다. 즉, 한 스레드가 락을 해제할 때까지 다른 스레드는 락을 획득할 수 없고, 락을 얻으면 그 다음에 작업을 실행합니다.

#### 코드 예시:
```java
worker1 = new Worker(outerLock);
worker2 = new Worker(outerLock);
```
이 경우, `worker1`과 `worker2`는 **같은 락(outerLock)**을 사용하고 있습니다. 따라서, 두 스레드는 동시에 실행되지 않고, **한 스레드가 락을 해제할 때**까지 다른 스레드는 기다려야 합니다. 이로 인해 스레드들은 **순차적으로 실행**됩니다.

### 3) **Share Lock Across Instances (락을 다른 인스턴스 간에 공유하는 경우)**
락을 다른 인스턴스 간에 공유한다는 것은 모든 스레드가 **하나의 고정된(static) 락**을 공유하여 사용하는 경우입니다. 이 경우, 각 스레드는 동일한 락을 사용하기 때문에, 여러 인스턴스에서 락을 경합해야 하며, 하나의 스레드가 락을 획득할 때 다른 모든 스레드는 기다려야 합니다.

#### 코드 예시:
```java
worker1 = new Worker(staticLock);
worker2 = new Worker(staticLock);
```
여기서 `worker1`과 `worker2`는 **모든 인스턴스에서 동일한 락(staticLock)**을 공유합니다. 따라서 여러 인스턴스의 스레드들 사이에서 락 경합이 발생하며, 스레드들은 동시에 실행될 수 없습니다. 한 스레드가 락을 획득하면, 다른 스레드들은 그 락이 해제될 때까지 대기해야 합니다.

### 요약:
- **동시에 실행(run concurrently)**: 각 스레드가 **서로 다른 락**을 사용할 때 가능하며, 스레드들은 동시에 실행됩니다.
- **순차적으로 실행(run sequentially)**: **같은 락**을 공유하는 스레드들은 하나의 스레드가 락을 해제할 때까지 다른 스레드가 대기해야 하므로, 순차적으로 실행됩니다.
- **락 공유(share lock)**: **여러 인스턴스가 동일한 static 락**을 공유하면, 모든 스레드들이 해당 락을 경합하게 되고, 동시에 실행되지 않습니다.

 

 

==============================================================

 

 

'미국유학 > CS 601 Principles of Software Dev' 카테고리의 다른 글

10.4 Lock  (0) 2024.10.05
241001 수업 , Thread  (0) 2024.10.02
Polymorphism (240905)  (0) 2024.09.06
240903 Iterator  (0) 2024.09.04
1st quiz  (0) 2024.08.30