diff --git a/study/ch04.md b/study/ch04.md new file mode 100644 index 0000000..2eda671 --- /dev/null +++ b/study/ch04.md @@ -0,0 +1,298 @@ +### 4.1 스레드 안전한 클래스 설계하기 + +- 상태에 대한 캡슐화, 소유권에 대한 이야기 +- stateful한 객체의 동시 접근을 관리하기 위한 방법 고려(기본형, 참조형) + - 불변, lock, 스레드 독점 방법 등 +- 소유권 분리? + - ex) `ServletContext` + - 애플리케이션 전체 범위에서 공유되는 객체를 저장할 수 있음 + - ServletContext 자체는 동기화를 보장해주지만, 안에 들어가는 객체들은 **사용자가 안전하게 공유**해야 한다. + +image + + + +```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 하거나, 해당 객체를 변경하는 부분에 동기화를 해줘야한다. + + + +image +> 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 mySet = new HashSet(); + + public synchronized void addPerson(Person p) { + mySet.add(p); + } + public synchronized boolean containsPerson(Person p) { + return mySet.contains(p); + } + + } + ``` + +- 자바 라이브러리의 대표적인 예시 + - 스레드 안전하지않은 `HashMap`, `ArrayList` 등의 스레드 안전성을 확보 + + ```jsx + List list = Collections.synchronizedList(new ArrayList<>()); + + // 안전 + list.add("A"); + list.get(0); + ``` + + +image + +image + + +image + + +왜 굳이 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) + +image + + + +```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"); + } + } +} +``` + + +image + + +인터프리터가 바이트코드를 실행하며 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 locations; + private final Map unmodifiableMap; + + // Point는 불변객체 + public DelegatingVehicleTracker(Map points) { + locations = new ConcurrentHashMap<>(points); + // 불변객체이기에, deepCopy 할필요가 없음 + unmodifiableMap = Collections.unmodifiableMap(locations); + } + + public Map 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 extends Vector { + // 추가기능 + public synchronized boolean putIfAbsent(E x) { + boolean absent = !contains(x); + if (absent){ + add(x); + } + return absent; + } + } + ``` + +- 클라이언트 측 락킹 + + ```java + @ThreadSafe + public class ListHelper { + public List list = Collections.synchronizedList(new ArrayList()); + + public synchronized boolean putIfAbsent(E x) { + boolean absent = !list.contains(x); + if (absent) + list.add(x); + return absent; + } + } + ``` + + +### 4.5 문서화 + +## ✅ 문서화해야 할 것들 + +- **클라이언트를 위한 스레드 안전 보장 내용** +- 문서화 타이밍은 설계 당시가 가장 좋음 + +## ✅ 동기화 정책 설계 시 결정해야 할 것들 + +- 어떤 변수는 `volatile`로 할지 +- 어떤 변수는 락으로 보호할지 +- 어떤 락이 어떤 변수를 보호하는지 +- 어떤 변수는 불변으로 만들지 +- 어떤 연산은 원자적으로 처리해야 하는지 + +## ✅ 안 좋은 현실 +- 많은 Java 명세(예: Servlet, JDBC)는 스레드 안전 보장이나 요구 사항을 거의 문서화하지 않음 +- 그래서 개발자는 어쩔 수 없이 **추측**해야 하는 상황에 놓임