Skip to content

Commit

Permalink
[#78][B팀] 공유 중인 가변 데이터는 동기화해 사용하라
Browse files Browse the repository at this point in the history
[#78][B팀] 공유 중인 가변 데이터는 동기화해 사용하라
  • Loading branch information
ksy90101 authored Apr 3, 2021
2 parents 6252317 + 46cce64 commit e83809e
Showing 1 changed file with 203 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라

# 결론

**여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야 한다.**

- 스레드간 공유되는 가변 데이터의 동기화를 하지 않거나 실패하면, 프로그램이 응답 불가 상태에 빠지거나 안전 실패(Safety failure)에 빠질 수 있다.
- 이러한 문제의 디버깅 난이도는 가장 어려운 축에 속한다.

베타적 실행은 필요없고, 스레드끼리의 통신만 필요하다면 `volatile` 한정자만으로도 동기화 할 수 있다.

- 하지만 올바르게 사용하기가 어렵다!

---



# 동기화

Mutli-thread를 잘 사용하면 좋은 성능을 낼 수 있다. 하지만 스레드간 동기화는 반드시 고려해야한다. 만약 동기화를 고려하지 않는다면, **서로 공유하는 데이터에 대해 안정성과 신뢰성을 보장할 수 없고, 이는 곧 프로그램의 일관성 및 안정성을 보장할 수 없다는 것**이다.

동기화에는 2가지 중요한 기능이 있다.

**1. 배타적 실행(Exclusion): 한 스레드가 객체를 변경하는 중이라, 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는 것**
- 한 객체가 일관된 상태를 가지고 생성되고, 이 객체에 접근하는 메서드는 그 객체에 락을 건다. 락을 건 메서드는 객체의 상태를 확인하고 필요하면 수정한다.
- → 일관된 상태에서 다른 일관된 상태로 변화시킴으로서 이 객체의 상태가 일관되지 않은 순간을 볼 수 없게 한다.

**2. 스레드간의 통신(Communication): 한 스레드가 만든 변화를 다른 스레드에서 볼 수 있게 하는 것**

- 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 수정의 **최종결과**를 보게 해준다.

---

# Java synchronized

Java에서는 `synchronized` 키워드를 통해 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장함으로써 동기화를 지원하고 있다.

---

언어 명세상 long, double 외의 변수를 읽고 쓰는 동작은 원자적(Atomic)이다.

- 여러 스레드가 같은 변수를 **동기화 없이 수정하는 중**이라도, 항상 어떤 스레드가 **정상적으로 저장한 값을 온전히 읽어옴을 보장**한다는 것

<br>

"엥? 그럼 성능 높이려면 원자적인 데이터 읽고 쓸 땐 동기화 안해야겠다!"

- 자바는 **한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.**
- 즉, 이러한 생각은 위험한 생각!

# 예제

Thread를 작동 중 멈추고 싶다.

- `Thread.stop` ? : 사용해서는 안된다. 안전하지 않아 사용이 금지되었다.
- `Thread.stop(Throwable obj)` 는 Java 11에서 제거되었다. 신기한건 96년도에 만들어졌고 98년도에 자제 API로 지정되었는데 Java11이 되어서 삭제되었다. 즉, 20년이 더 걸렸다.
- → 공개 API는 신중히 설계해야한다.... (Thread.stop() 아직까지도 남아있다고 함)

<br>

그럼, 올바르게 삭제하기 위해선 어떻게 해야 하는가?

- `boolean` 필드를 두고 false로 초기화한 후 반복문을 돌게 만든다. 그리고 다른 스레드에서 해당 스레드를 멈추게 하고 싶은 경우에 true로 초기화하게 만들어 멈추게 하자.

```java
public class StopThread {
private static boolean stopRequested; // false 초기화
public static void main(String[] args) throws InterruptedException {

new Thread(() -> {
int i = 0;
while(!stopRequested){
i++;
}
}).start();

// 1초 뒤에 저 스레드를 멈추게 하자.
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
```

<br>
<br>
<br>


**?? 왜 무한 루프지?**

- 동기화의 두번째 기능. **스레드간의 통신**을 생각하지 않았기 때문이다.
- 동기화하지 않으면, 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다 !
- `synchronized` 를 사용해 동기화시키자.

```java
public class StopThread {
private static boolean stopRequested;

// 쓰기 메서드
private static synchronized void requestStop() {
stopRequested = true;
}

// 읽기 메서드
private static synchronized boolean stopRequested() {
return stopRequested;
}

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

TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
```

쓰기 메서드와 읽기 메서드를 둘 다 동기화했음을 주목하자.

- **쓰기 메서드, 읽기 메서드 둘 다 동기화하지 않으면 동작을 보장하지 않는다. 스레드간의 통신(Communication)의 동작을 보장하지 못한다.**

<br>
<br>
<br>

위 코드에 `volatile` 을 사용하면 더 간단하게 작성할 수 있다.

```java
public class StopThread {
// volatile 사용
private static volatile boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
}).start();

TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
```

`volatile` 이 뭐야?: **데이터를 메인 메모리에 저장하겠다!**

- '응? 그럼 그 전엔 메인 메모리에 저장안했나?'
- **Multi-thread 환경**에서는 task를 수행하는 동안 성능 향상을 위해 변수 데이터들을 Main memory에서 활용하는 것이 아닌 **CPU cache에 저장하고 활용**한다.

![1](https://user-images.githubusercontent.com/37873745/112991414-0c2df500-91a2-11eb-89e3-0613139d33ef.png)

- 이런 구조였기에 Thread 간에 같은 변수에 접근하더라도 같은 값을 가지지 않고, 다른 값을 가질 수 있었던 것 → 즉, 동기화 문제의 이유 중 하나
- 즉, `volatile`이 사용된 필드는 Multi thread 상황에서도 cache를 사용하지 않고 Main memory에 데이터를 저장하고 읽기에 위 문제 상황은 해결할 수 있다.

<br>
<br>

그럼 애매한 건 다 `volatile` 써서 동기화 문제를 해결할 수 있는 것 아닐까?

- 아니다! Write을 하기 이전에 Read를 해버리면 잘못된 값을 읽어올 수 있다.

```java
private static volatile int nextSerialNumber= 0;

public static int generateSerialNumber(){
return nextSerialNumber++;
}
```

- 각 Thread는 `generateSerialNumber` 값이 unique 값이라고 생각할 수 있지만, 한 Thread가 저장하기 이전에 읽어버리면 Thread 간 `generateSerialNumber`는 중복된 값이 된다 → Safety Failure (안전실패) 오류
- 이 문제도 `synchronized` 를 붙이면 해결되며, 이러한 경우 `volatile` 메서드를 삭제하여야 한다.

<br>
<br>

근데 보통 Primitive, Collection 같이 자주 사용되는 것에는 동기화를 지원하는 클래스들이 있다. → `java.util.concurrent` 참조

- 작성하기 편하다. `synchronized` 같은 키워드를 사용하지 않아도 된다.
- 성능도 기존 동기화를 수동으로 하는 것보다 우수하다.

<br>
<br>

------

# 하지만, 이러한 동기화 문제를 피하는 가장 좋은 방법은...

애초에 가변 데이터를 공유하지 않는 것...

- **불변 데이터만 공유하거나 아무것도 공유하지 말자 (Item 17)**
- **가변 데이터는 단일 스레드에서만 사용하도록 하자.**
- **문서화를 잘해서 동기화 문제가 일어나지 않도록 하자**
- `synchronized` 를 사용했다면, 다른 부분에서도 사용할 수 있도록
- 가변 데이터는 단일 스레드에서만 사용한다면, 멀티 스레드에서 사용하지 않도록 등
- **사용하려는 프레임워크, 라이브러리가 스레드를 수행하는지 인지하고 사용하자.**

0 comments on commit e83809e

Please sign in to comment.