Skip to content

Commit 3be2199

Browse files
authored
Merge pull request #2 from youngreal/main
3주차: ch4 - 객체 구성(이영진)
2 parents 1646b18 + 786a79a commit 3be2199

File tree

1 file changed

+298
-0
lines changed

1 file changed

+298
-0
lines changed

study/ch04.md

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
### 4.1 스레드 안전한 클래스 설계하기
2+
3+
- 상태에 대한 캡슐화, 소유권에 대한 이야기
4+
- stateful한 객체의 동시 접근을 관리하기 위한 방법 고려(기본형, 참조형)
5+
- 불변, lock, 스레드 독점 방법 등
6+
- 소유권 분리?
7+
- ex) `ServletContext`
8+
- 애플리케이션 전체 범위에서 공유되는 객체를 저장할 수 있음
9+
- ServletContext 자체는 동기화를 보장해주지만, 안에 들어가는 객체들은 **사용자가 안전하게 공유**해야 한다.
10+
11+
<img width="1407" height="139" alt="image" src="https://github.com/user-attachments/assets/1c283f44-740f-46bf-846a-0fd13790e251" />
12+
13+
14+
15+
```java
16+
attributes.put(name, value); // 안전
17+
attributes.get(name) // 안전
18+
```
19+
20+
21+
22+
**저장된 객체 자체의 동시성은 컨테이너가 보장하지 않음**
23+
24+
→ 여러 서블릿이 꺼내서 동시에 사용하는 경우는 **개발자가 직접 동기화**
25+
26+
```java
27+
// 안전
28+
context.setAttribute("userService", new UserService());
29+
UserService service = (UserService) context.getAttribute("userService");
30+
31+
// 위험, 개발자가 동기화에 신경써야함
32+
service.updateUserData(); // 다중 스레드에서 호출될 수 있음
33+
```
34+
35+
- `HttpSession`은 세션 복제나 패시베이션 과정에서 컨테이너가 직접 접근하므로 그 안의 객체들도 **스레드 안전**해야 합니다.
36+
- [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)
37+
- 사용되지 않는 세션을 메모리에서 제거하고 영구 스토리지에 저장할 수 있음
38+
- 스레드 A가 `HttpSession`에 mutable한 `ShoppingCart` 객체 변경 후 저장 →
39+
웹 컨테이너의 내부 스레드가 세션 복제를 위해 `ShoppingCart` 객체를 직렬화
40+
**일관되지않은 상태가 복제**
41+
- HttpSession에 저장되는 객체는 자체적으로 thread-safe 하거나, 해당 객체를 변경하는 부분에 동기화를 해줘야한다.
42+
43+
44+
45+
<img width="1364" height="199" alt="image" src="https://github.com/user-attachments/assets/b1f0d0b1-eeed-4c4e-a121-ef57a71502ce" />
46+
> HttpSession 클래스도 ConcurrentHashMap으로 동기화 보장
47+
48+
- [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)
49+
- 특정 WAS에 저장된 객체(ShoppingCart)를 다른 WAS로 복제(replication) 할 수 있음 ⇒ 싱크 불일치 가능성
50+
51+
- 정리
52+
- HttpSession, ServletContext는 둘다 객체를 읽고 쓰는부분에 대한 동기화는 보장한다.
53+
- **저장된 객체의 상태변경시 동기화전략을 고려해야하는건 사용개발자의 몫이다**.
54+
55+
56+
### 4.2 인스턴스 한정
57+
- 결국 데이터 공개범위와 thread-safe와 연관된 이야기
58+
- 책 코드, mySet을 노출시키지 않는다.
59+
60+
```jsx
61+
@ThreadSafe
62+
public class PersonSet {
63+
64+
@GuardedBy("this")
65+
private final Set<Person> mySet = new HashSet<Person>();
66+
67+
public synchronized void addPerson(Person p) {
68+
mySet.add(p);
69+
}
70+
public synchronized boolean containsPerson(Person p) {
71+
return mySet.contains(p);
72+
}
73+
74+
}
75+
```
76+
77+
- 자바 라이브러리의 대표적인 예시
78+
- 스레드 안전하지않은 `HashMap`, `ArrayList` 등의 스레드 안전성을 확보
79+
80+
```jsx
81+
List<String> list = Collections.synchronizedList(new ArrayList<>());
82+
83+
// 안전
84+
list.add("A");
85+
list.get(0);
86+
```
87+
88+
89+
<img width="1419" height="171" alt="image" src="https://github.com/user-attachments/assets/f3d3af3a-c3f5-42ca-8bb2-38f9411847a8" />
90+
91+
<img width="1012" height="890" alt="image" src="https://github.com/user-attachments/assets/37ab2de1-deb8-4dcd-ad26-7ca248d1710d" />
92+
93+
94+
<img width="741" height="703" alt="image" src="https://github.com/user-attachments/assets/33a4f8d8-727b-4170-9939-553f0572f01e" />
95+
96+
97+
왜 굳이 mutex 변수를 썼을까? this라던가.. (락 획득을 캡슐화)
98+
- 일단, Synchronized를 블록단위로 쓰려면 객체가 필요
99+
- 외부 코드가 `this` 객체로 synchronized를 잡는 것을 방지하기 위해
100+
- this: 외부에서 락을 잡을 수 있어 의도치 않은 경합 발생 가능
101+
- mutex: 외부에서 접근 불가 — 락 충돌 위험 차단
102+
103+
104+
### 모니터패턴(monitorenter) , synchronized
105+
106+
[VM Spec Compiling for the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530)
107+
108+
<img width="1076" height="632" alt="image" src="https://github.com/user-attachments/assets/93ab365c-f3f6-4f01-9d32-b1b90b6d6984" />
109+
110+
111+
112+
```java
113+
public class SynchronizedMain {
114+
private static final Object lock = new Object();
115+
116+
public static void main(String[] args) {
117+
someMethod();
118+
}
119+
120+
private static void someMethod() {
121+
synchronized (lock) {
122+
System.out.println("hello");
123+
}
124+
}
125+
}
126+
```
127+
128+
129+
<img width="1048" height="898" alt="image" src="https://github.com/user-attachments/assets/93a351e7-310c-47bd-9bce-90a583d05c77" />
130+
131+
132+
인터프리터가 바이트코드를 실행하며 monitorenter 바이트코드가 C++의 InterPreterRuntime::monitorenter()를 호출한다.
133+
134+
문서들에 바이트코드의 의미만 정의하고 ,어떤 C++ 코드를 호출하는지는 다루는지에 대한 부분을 못찾음. `monitorenter`가 락을 건다는 것까진 **공식적으로 명세**되어 있음
135+
>> monitor entry on invocation of a method is handled implicitly by the Java virtual machine's method invocation instructions. (JVM의 내부적 지시에 따라 처리된다)
136+
137+
138+
GPT
139+
바이트코드 → 인터프리터 처리되는 과정의 소스로 증거 찾을수 있다… monitorenter를 찾아보자
140+
https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/share/interpreter/interpreterRuntime.cpp#L622
141+
ObjectSynchronizer::enter를 호출하고 이는 C++ 코드를 호출한다.
142+
143+
144+
synchronized 동작(ObjectSynchronizer::enter_for을 찾아보자)
145+
https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/synchronizer.cpp
146+
147+
148+
매우 간단하게 요약하면..(JVM 경량모드가 아닌경우 가정)
149+
```java
150+
ObjectSynchronizer::enter_for(obj)
151+
└─ enter_fast_impl(obj, lock, thread)
152+
├─ CAS로 obj의 mark word를 검사하고 락 시도
153+
├─ CAS 성공 → 경량 락 획득 → Synchronized 블록 내부로 진입
154+
└─ CAS 실패 → return false
155+
156+
└─ inflate_for(thread, obj)
157+
└─ ObjectMonitor 생성 또는 기존 모니터 획득
158+
└─ inflate_impl()
159+
└─ 무한루프, CAS를 하며 ObjectMonitor 생성 또는 기존 모니터 획득
160+
└─ 다른스레드가 락을소유중이면 entryList에 들어가고 moniterexit가 호출되어 notify()를 통해 스레드를 깨우고 진입.
161+
```
162+
163+
164+
경량모드일때의 동작
165+
[jdk/src/hotspot/share/runtime/lightweightSynchronizer.cpp at master · openjdk/jdk](https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/lightweightSynchronizer.cpp)
166+
경량 모드인지 아닌지는 JVM이 런타임에 모드를 선택한다. (해당 Synchronized에대한 경합이 강한지 아닌지에 따라)
167+
168+
169+
### 4.3 스레드 안전성 위임
170+
- AtomicLong, CopyOnWriteArrayList, ConcurrentHashMap 등 thread-safe를 위임하는 것을 말함
171+
- 성공 예시 (각 이벤트 리스너를 CopyOnWriteArrayList에 위임하거나, AtomicLong, ConcurrentHashMap등에 위임
172+
173+
```java
174+
@ThreadSafe
175+
public class DelegatingVehicleTracker {
176+
private final ConcurrentMap<String, Point> locations;
177+
private final Map<String, Point> unmodifiableMap;
178+
179+
// Point는 불변객체
180+
public DelegatingVehicleTracker(Map<String, Point> points) {
181+
locations = new ConcurrentHashMap<>(points);
182+
// 불변객체이기에, deepCopy 할필요가 없음
183+
unmodifiableMap = Collections.unmodifiableMap(locations);
184+
}
185+
186+
public Map<String, Point> getLocations() {
187+
return unmodifiableMap;
188+
}
189+
190+
public Point getLocation(String id) {
191+
return locations.get(id);
192+
}
193+
}
194+
```
195+
196+
- 위임 실패 예시(각 상태변수 간 관계가 있는경우)
197+
198+
```java
199+
public class NumberRange {
200+
private final AtomicInteger lower = new AtomicInteger(0);
201+
private final AtomicInteger upper = new AtomicInteger(0);
202+
203+
public void setLower(int i) {
204+
//위험
205+
if (i > upper.get())
206+
throw new IllegalArgumentException(
207+
"can't set lower to " + i + " > upper");
208+
lower.set(i);
209+
}
210+
211+
public void setUpper(int i) {
212+
//위험
213+
if (i < lower.get())
214+
throw new IllegalArgumentException(
215+
"can't set upper to " + i + " < lower");
216+
upper.set(i);
217+
}
218+
219+
public boolean isInRange(int i) {
220+
return (i >= lower.get() && i <= upper.get());
221+
}
222+
}
223+
```
224+
225+
- 내부 상태를 안전하게 공개해도 되는경우
226+
227+
```java
228+
@ThreadSafe
229+
public class SafePoint {
230+
@GuardedBy("this") private int x, y;
231+
232+
public SafePoint(int x, int y) { this.x = x; this.y = y; }
233+
234+
public synchronized int[] get() { return new int[] { x, y }; }
235+
236+
public synchronized void set(int x, int y) { this.x = x; this.y = y; }
237+
}
238+
```
239+
240+
241+
### 4.4 스레드 안전하게 구현된 클래스에 기능추가
242+
243+
| 방법 | 특징 | 위험 |
244+
| --- | --- | --- |
245+
| 클래스 확장 | 편리하지만 깨질 수 있음 | 부모 클래스 락 정책 변경시 위험 |
246+
| 클라이언트 측 락킹 | 외부에서 락을 맞추는 방식 | 락 정책 변경시 깨질 위험 |
247+
| 합성 | 안정적, 권장 | 다소 번거로움 |
248+
- 클래스 확장
249+
250+
```java
251+
@ThreadSafe
252+
public class BetterVector<E> extends Vector<E> {
253+
// 추가기능
254+
public synchronized boolean putIfAbsent(E x) {
255+
boolean absent = !contains(x);
256+
if (absent){
257+
add(x);
258+
}
259+
return absent;
260+
}
261+
}
262+
```
263+
264+
- 클라이언트 측 락킹
265+
266+
```java
267+
@ThreadSafe
268+
public class ListHelper<E> {
269+
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
270+
271+
public synchronized boolean putIfAbsent(E x) {
272+
boolean absent = !list.contains(x);
273+
if (absent)
274+
list.add(x);
275+
return absent;
276+
}
277+
}
278+
```
279+
280+
281+
### 4.5 문서화
282+
283+
## ✅ 문서화해야 할 것들
284+
285+
- **클라이언트를 위한 스레드 안전 보장 내용**
286+
- 문서화 타이밍은 설계 당시가 가장 좋음
287+
288+
## ✅ 동기화 정책 설계 시 결정해야 할 것들
289+
290+
- 어떤 변수는 `volatile`로 할지
291+
- 어떤 변수는 락으로 보호할지
292+
- 어떤 락이 어떤 변수를 보호하는지
293+
- 어떤 변수는 불변으로 만들지
294+
- 어떤 연산은 원자적으로 처리해야 하는지
295+
296+
## ✅ 안 좋은 현실
297+
- 많은 Java 명세(예: Servlet, JDBC)는 스레드 안전 보장이나 요구 사항을 거의 문서화하지 않음
298+
- 그래서 개발자는 어쩔 수 없이 **추측**해야 하는 상황에 놓임

0 commit comments

Comments
 (0)