-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#78][B팀] 공유 중인 가변 데이터는 동기화해 사용하라
- Loading branch information
Showing
1 changed file
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` 를 사용했다면, 다른 부분에서도 사용할 수 있도록 | ||
- 가변 데이터는 단일 스레드에서만 사용한다면, 멀티 스레드에서 사용하지 않도록 등 | ||
- **사용하려는 프레임워크, 라이브러리가 스레드를 수행하는지 인지하고 사용하자.** |