Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 298 additions & 0 deletions study/ch04.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
### 4.1 스레드 안전한 클래스 설계하기

- 상태에 대한 캡슐화, 소유권에 대한 이야기
- stateful한 객체의 동시 접근을 관리하기 위한 방법 고려(기본형, 참조형)
- 불변, lock, 스레드 독점 방법 등
- 소유권 분리?
- ex) `ServletContext`
- 애플리케이션 전체 범위에서 공유되는 객체를 저장할 수 있음
- ServletContext 자체는 동기화를 보장해주지만, 안에 들어가는 객체들은 **사용자가 안전하게 공유**해야 한다.

<img width="1407" height="139" alt="image" src="https://github.com/user-attachments/assets/1c283f44-740f-46bf-846a-0fd13790e251" />



```java
attributes.put(name, value); // 안전
attributes.get(name) // 안전
```



**저장된 객체 자체의 동시성은 컨테이너가 보장하지 않음**

→ 여러 서블릿이 꺼내서 동시에 사용하는 경우는 **개발자가 직접 동기화**

```java
// 안전
context.setAttribute("userService", new UserService());
UserService service = (UserService) context.getAttribute("userService");

// 위험, 개발자가 동기화에 신경써야함
service.updateUserData(); // 다중 스레드에서 호출될 수 있음
```

- `HttpSession`은 세션 복제나 패시베이션 과정에서 컨테이너가 직접 접근하므로 그 안의 객체들도 **스레드 안전**해야 합니다.
- [15.2. HttpSession Passivation and Activation | HTTP Connectors Load Balancing Guide | Red Hat JBoss Web Server | 1.0 | Red Hat Documentation](https://docs.redhat.com/en/documentation/red_hat_jboss_web_server/1.0/html/http_connectors_load_balancing_guide/clustering-http-passivation)
- 사용되지 않는 세션을 메모리에서 제거하고 영구 스토리지에 저장할 수 있음
- 스레드 A가 `HttpSession`에 mutable한 `ShoppingCart` 객체 변경 후 저장 →
웹 컨테이너의 내부 스레드가 세션 복제를 위해 `ShoppingCart` 객체를 직렬화
→ **일관되지않은 상태가 복제**
- HttpSession에 저장되는 객체는 자체적으로 thread-safe 하거나, 해당 객체를 변경하는 부분에 동기화를 해줘야한다.



<img width="1364" height="199" alt="image" src="https://github.com/user-attachments/assets/b1f0d0b1-eeed-4c4e-a121-ef57a71502ce" />
> HttpSession 클래스도 ConcurrentHashMap으로 동기화 보장

- [Chapter 17. HTTP session state replication | HTTP Connectors Load Balancing Guide | Red Hat JBoss Enterprise Application Platform | 5 | Red Hat Documentation](https://docs.redhat.com/en/documentation/JBoss_Enterprise_Application_Platform/5/html/http_connectors_load_balancing_guide/clustering-http-state)
- 특정 WAS에 저장된 객체(ShoppingCart)를 다른 WAS로 복제(replication) 할 수 있음 ⇒ 싱크 불일치 가능성

- 정리
- HttpSession, ServletContext는 둘다 객체를 읽고 쓰는부분에 대한 동기화는 보장한다.
- **저장된 객체의 상태변경시 동기화전략을 고려해야하는건 사용개발자의 몫이다**.


### 4.2 인스턴스 한정
- 결국 데이터 공개범위와 thread-safe와 연관된 이야기
- 책 코드, mySet을 노출시키지 않는다.

```jsx
@ThreadSafe
public class PersonSet {

@GuardedBy("this")
private final Set<Person> mySet = new HashSet<Person>();

public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}

}
```

- 자바 라이브러리의 대표적인 예시
- 스레드 안전하지않은 `HashMap`, `ArrayList` 등의 스레드 안전성을 확보

```jsx
List<String> list = Collections.synchronizedList(new ArrayList<>());

// 안전
list.add("A");
list.get(0);
```


<img width="1419" height="171" alt="image" src="https://github.com/user-attachments/assets/f3d3af3a-c3f5-42ca-8bb2-38f9411847a8" />

<img width="1012" height="890" alt="image" src="https://github.com/user-attachments/assets/37ab2de1-deb8-4dcd-ad26-7ca248d1710d" />


<img width="741" height="703" alt="image" src="https://github.com/user-attachments/assets/33a4f8d8-727b-4170-9939-553f0572f01e" />


왜 굳이 mutex 변수를 썼을까? this라던가.. (락 획득을 캡슐화)
- 일단, Synchronized를 블록단위로 쓰려면 객체가 필요
- 외부 코드가 `this` 객체로 synchronized를 잡는 것을 방지하기 위해
- this: 외부에서 락을 잡을 수 있어 의도치 않은 경합 발생 가능
- mutex: 외부에서 접근 불가 — 락 충돌 위험 차단


### 모니터패턴(monitorenter) , synchronized

[VM Spec Compiling for the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530)

<img width="1076" height="632" alt="image" src="https://github.com/user-attachments/assets/93ab365c-f3f6-4f01-9d32-b1b90b6d6984" />



```java
public class SynchronizedMain {
private static final Object lock = new Object();

public static void main(String[] args) {
someMethod();
}

private static void someMethod() {
synchronized (lock) {
System.out.println("hello");
}
}
}
```


<img width="1048" height="898" alt="image" src="https://github.com/user-attachments/assets/93a351e7-310c-47bd-9bce-90a583d05c77" />


인터프리터가 바이트코드를 실행하며 monitorenter 바이트코드가 C++의 InterPreterRuntime::monitorenter()를 호출한다.

문서들에 바이트코드의 의미만 정의하고 ,어떤 C++ 코드를 호출하는지는 다루는지에 대한 부분을 못찾음. `monitorenter`가 락을 건다는 것까진 **공식적으로 명세**되어 있음
>> monitor entry on invocation of a method is handled implicitly by the Java virtual machine's method invocation instructions. (JVM의 내부적 지시에 따라 처리된다)


GPT
바이트코드 → 인터프리터 처리되는 과정의 소스로 증거 찾을수 있다… monitorenter를 찾아보자
https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/share/interpreter/interpreterRuntime.cpp#L622
ObjectSynchronizer::enter를 호출하고 이는 C++ 코드를 호출한다.


synchronized 동작(ObjectSynchronizer::enter_for을 찾아보자)
https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/synchronizer.cpp


매우 간단하게 요약하면..(JVM 경량모드가 아닌경우 가정)
```java
ObjectSynchronizer::enter_for(obj)
└─ enter_fast_impl(obj, lock, thread)
├─ CAS로 obj의 mark word를 검사하고 락 시도
├─ CAS 성공 → 경량 락 획득 → Synchronized 블록 내부로 진입
└─ CAS 실패 → return false
└─ inflate_for(thread, obj)
└─ ObjectMonitor 생성 또는 기존 모니터 획득
└─ inflate_impl()
└─ 무한루프, CAS를 하며 ObjectMonitor 생성 또는 기존 모니터 획득
└─ 다른스레드가 락을소유중이면 entryList에 들어가고 moniterexit가 호출되어 notify()를 통해 스레드를 깨우고 진입.
```


경량모드일때의 동작
[jdk/src/hotspot/share/runtime/lightweightSynchronizer.cpp at master · openjdk/jdk](https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/lightweightSynchronizer.cpp)
경량 모드인지 아닌지는 JVM이 런타임에 모드를 선택한다. (해당 Synchronized에대한 경합이 강한지 아닌지에 따라)


### 4.3 스레드 안전성 위임
- AtomicLong, CopyOnWriteArrayList, ConcurrentHashMap 등 thread-safe를 위임하는 것을 말함
- 성공 예시 (각 이벤트 리스너를 CopyOnWriteArrayList에 위임하거나, AtomicLong, ConcurrentHashMap등에 위임

```java
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;

// Point는 불변객체
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<>(points);
// 불변객체이기에, deepCopy 할필요가 없음
unmodifiableMap = Collections.unmodifiableMap(locations);
}

public Map<String, Point> getLocations() {
return unmodifiableMap;
}

public Point getLocation(String id) {
return locations.get(id);
}
}
```

- 위임 실패 예시(각 상태변수 간 관계가 있는경우)

```java
public class NumberRange {
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);

public void setLower(int i) {
//위험
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i);
}

public void setUpper(int i) {
//위험
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
upper.set(i);
}

public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
```

- 내부 상태를 안전하게 공개해도 되는경우

```java
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;

public SafePoint(int x, int y) { this.x = x; this.y = y; }

public synchronized int[] get() { return new int[] { x, y }; }

public synchronized void set(int x, int y) { this.x = x; this.y = y; }
}
```


### 4.4 스레드 안전하게 구현된 클래스에 기능추가

| 방법 | 특징 | 위험 |
| --- | --- | --- |
| 클래스 확장 | 편리하지만 깨질 수 있음 | 부모 클래스 락 정책 변경시 위험 |
| 클라이언트 측 락킹 | 외부에서 락을 맞추는 방식 | 락 정책 변경시 깨질 위험 |
| 합성 | 안정적, 권장 | 다소 번거로움 |
- 클래스 확장

```java
@ThreadSafe
public class BetterVector<E> extends Vector<E> {
// 추가기능
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent){
add(x);
}
return absent;
}
}
```

- 클라이언트 측 락킹

```java
@ThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());

public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
```


### 4.5 문서화

## ✅ 문서화해야 할 것들

- **클라이언트를 위한 스레드 안전 보장 내용**
- 문서화 타이밍은 설계 당시가 가장 좋음

## ✅ 동기화 정책 설계 시 결정해야 할 것들

- 어떤 변수는 `volatile`로 할지
- 어떤 변수는 락으로 보호할지
- 어떤 락이 어떤 변수를 보호하는지
- 어떤 변수는 불변으로 만들지
- 어떤 연산은 원자적으로 처리해야 하는지

## ✅ 안 좋은 현실
- 많은 Java 명세(예: Servlet, JDBC)는 스레드 안전 보장이나 요구 사항을 거의 문서화하지 않음
- 그래서 개발자는 어쩔 수 없이 **추측**해야 하는 상황에 놓임