Item86. Serializable 구현할지는 신중히 결정하라

어떤 클래스의 객체를 직렬화 할 수 있게 하려면 클래스 선언에 implments Serializable만 덧붙이면 된다.

Serializable을 구현하면 release한뒤에는 수정하기 어렵다. 클래스가 Serializable을 구현하면 직렬화된 byte stream 인코딩도 하나의 공개 API가 된다. (직렬화한 형태도 계속 지원해야 한다.)

또한 custom 직렬화 형태를 설계하지않고, default로 두게 되면 private와 package-private 필드마저도 공개가 된다. 따라서 직렬화를 한다고 하면 직렬화 형태도 주의해서 설계해야 한다고 한다.

직렬화의 단점

  • stream 고유 식별자 (직렬 버전 UID) : 모든 직렬화된 class는 고유 식별 번호를 부여받는다.
  1. seraialVersionUID라는 이름의 static final long 필드로 이 번호를 명시하지 않으면 시스템이 runtime에 암호 해시 함수(SHA-1)을 생성해 자동으로 클래스안에 넣는다. 이떄 클래스 멤버들이 고려되서 생성되기 떄문에 클래스가 수정되면 이 UID값도 같이 수정된다.

  2. 버그와 보안 구멍이 생길 위험이 높아진다. 역직렬화는 숨은 생성자를 통해 객체를 만들며, 클래스 불변식 꺠짐과 허가되지 않은 접근에 쉽게 노출된다는 단점을 가지고 있다.

  3. 해당 class의 신버전을 release할때 테스트할점이 늘어난다.
    직렬화 가능한 class가 수정되면 신버전 class도 구버전 직렬화 형태로 역직렬화 되는지, 직렬화 되는지 모두 테스트해보아야 한다. 따라서 매 release 횟수에 비례하여 테스트양이 증가한다.

일반적인 사용 사례

  • BigInteger,Instant와 같은 값 타입 클래스와 Collection 클래스들은 Serializable을 구현하고, Thread Pool 처럼 동작하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않는다고 한다.
Read more

Item 85. Java 직렬화의 대안을 찾으라

직렬화, 역직렬화

  • 직렬화 : Java가 객체를 byte stream으로 인코딩
  • 역직렬화 : byte stream을 객체로 encoding

역직렬화와 보안간의 관련성

  • 직렬화의 근본적인 문제는 공격범위가 넓다는 점이다. ObjectInputStream의 readObject method를 호출하면 객체 그래프가 역직렬화되기 때문이다.
    클래스패스 안의 Serilizable interface를 구현한 거의 모든 타입의 객체를 만들어낼 수 있기 떄문에, 바이트 스트림을 역직렬화하는 과정에서 이 method는 그 타입들 안의 모든 코드를 수행할 수 있다.

  • -> 그 타입들의 코드 전체가 공격범위에 들어간다.

  • gadget method : 객체 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 method

따라서 위험요소가 없는 byte stream만 역직렬화해야 한다.

그렇다하더라도 역직렬화에 시간이 오래걸리는 짧은 stream을 역직렬화 하는 것만으로도 서비스 거부 공격(denial-of-service,DoS)을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static byte[] bomb(){
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for(int i = 0; i<100;i++){
Set<Object> t1 = new HashSet<>()
Set<Object> t2 = new HashSet<>()
t1.add("foo")
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root);
}

위 코드는 201개의 HashSet 객체를 만드는데, 각각은 3개 이하의 객체 참조를 가진다. HashSet을 역직렬화하려면 각각 원소의 해시코드를 계산해야 하는데,
HashSet에 포함된 객체 참조 타입이 역시 HashSet임으로 depth가 100단계까지 만들어진다. 즉 2^100 이상으로 HashCode method를 호출해야 한다.

Read more

Item84. 프로그램의 동작을 Thread Scheduler 에 기대지 말라

  • 실행 가능한 Thread의 평균적인 수를 processor 수보다 지나치게 많아지지 않도록 유지하는게 좋다. 그래야만 Thread scheudling 정책이 상이한 시스템에도 다른 시스템들과 동작이 크게 달라지지 않는다.

실행 가능한 Thread 수를 적게 유지하는 기법 (불필요한 Thread 를 만들지 않는 방법)

  1. Thread 가 busy wait 상태가 되지 않게 한다. 즉 처리할 작업이 없다면 실행되지 않도록 한다.

공유 객체의 상태가 바뀔떄까지 쉬지 않고 검사해서는 안된다는 뜻이다. busy wait는 processror 에 큰 부담을 주어 다른 유용한 작업이 실행될 기회를 박탈한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SlowCountDownLatch {

private int count;

public SlowCountDownLatch(int count){
if(count < 0 ){
throw new IllegalArgumentException(count + " < 0");
}
this.count = count;
}

public void await(){
while (true){ // busy wait
synchronized (this){
if(count == 0){
return;
}
}
}
}

public synchronized void countDown(){
if(count != 0){
count --;
}
}
}

위의 예제코드는 불필요하게 공유 객체를 검사하며 기다리는 busy wait 예시이다.

Thread.yield

  • Thread.yield() method는 현재 thread 는 실행 대기 상태로 되고, 다른 우선순위가 높은 thread 를 실행시키는 method이다.
  • 저자는 Thread.yield() 는 단기적으로 현재 개발환경에서 성능을 올려줄수는 있지만, 이식성은 오히려 떨어지며 test가능한 수단도 없으므로 되도록이면 지양하라고 권고하고 있다.
Read more

Item83. 지연 초기화는 신중히 사용하라

지연 초기화

  • 지연 초기화 (lazy initalization) 는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 방법이다.

  • 값이 쓰이지 않으면 당연히 초기화도 일어나지 않으며, 지연 초기화는 정적 필드 (static) 와 인스턴스 필드에 모두 사용할 수 있다.

  • 지연 초기화를 함부러 사용하면 안되는게, 객체 생성시 초기화 비용은 최적화가 될 수 있으나 지연 초기화하는 필드에 추가로 접근해야 하는 비용이 생긴다.

1
2
3
4
String lateinitVar;
if(lateinitVar == null){
lateInitVar = "lazy initilization";
}

지연 초기화를 사용할 것으로 권장되는 상황은 다음과 같다.

  • 해당 클래스의 객체 중 그 필드를 사용하는 경우보다 필드를 초기화하는 비용이 더 큰 경우

만약 멀티 쓰레드 환경이라면 지연 초기화하려는 필드가 여러 쓰레드에 의해 공유되는 상황에서는 초기화 작업이 반드시 동기화하고 초기화해야 한다.

위 상황을 제외하고는 대부분의 상황에서는 일반적인 초기화가 지연초기화보다 낫다.

1
private final FieldType field = computeFieldValue(); //일반적인 초기화 
1
2
3
4
5
6
7
8
private FieldType field; //지연초기화 방식

private synchronized FieldType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
  • 만약 성능떄문에 정적필드를 지연 초기화해야 한다면 lazy initialization holder class 관용구를 사용하자
1
2
3
4
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {return FieldHolder.field;}

위처럼 전용 Holder class를 만들어두면 getField()method가 호출될떄 FieldHolder 클래스 초기화가 된다. 이렇게 사용하게 되면 클래스 초기화가 최초에 끝난 뒤에는 아무런 검사나 동기화 없이 필드에 접근하게 된다.

1
2
3
if(field == null){
field = computeFieldValue();
}

위와 같이 매번 필드에 접근해서 null여부를 확인하지 않아도 된다는 말이다.

  • 성능 떄문에 멤버필드를 지연 초기화해야 한다면 double check 관용구를 사용하자

이 방식은 필드의 값을 두 번 검사하는 방식으로 , 최초는 동기화 없이 검사하고 두 번쨰는 동기화하여 검사한다. 두번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화한다. 필드가 초기화된 이후에는 동기화하지 않으므로 해당 필드를 반드시 volatile(메인메모리로부터 저장하고 읽어옴) 로 선언해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

private volatile FieldType field;

private FieldType getField(){

// 첫번쨰 검사 (동기화 없이 검사)
FieldType result = field;
if( result != null){
return result;
}

// 두번쨰 검사 ( 초기화 안됬다면 동기화하여 초기화 )
synchronized(this){
if(field == null){
field = computeFieldValue();
}
return field;
}
}

위 코드에서 result 지역변수를 사용한 이유는 필드가 이미 초기화된 상황에서 필드를 한번만 읽도록 보장하기 위함이다.

Read more

Item82. Thread 안전성 수준을 문서화하라

  • 한 method가 여러 thread가 동시에 호출할때, 그 method가 어떻게 동작하는지는 해당 class와 이를 사용하는 client사이의 중요한 계약과 같다. API 문서에 아무런 설명조차 없으면 심각한 오류로 이어질 수 있다.

  • thread 안전성에도 수준이 나뉜다. 멀티 thread 환경에서도 API를 안전하게 사용하려면 클래스가 지원하는 thread 안전성 수준을 정확하게 명시해주어야 한다.

아래의 목록은 thread 안전성이 높은 순으로 나열한 것이다.

  1. 불변 ( Immutable ) : 동기화조차 필요없음. 객체의 상태가 변하지 않음이 보장됨. ex) String,Long,BigInteger Class

  2. 무조건적 스레드 안전 (Unconditionally Thread-Safe) :가변 객체임으로 객체의 상태는 수정될 수 있으나, 클래스 내부에서 완벽히 동기화하여 별도의 외부 동기화 없이 동시에 사용해도 안전하다. ex)Atmoic Long , ConcurrentHashMap

  3. 조건부 스레드 안전 (Conditionally Thread-Safe ) : 일부 method만 외부 동기화 작업 필요

  4. 스레드 안전하지 않음 (Not Thread-Safe ) : 외부 동기화 작업 필요 ex) 기본 Collection , ArrayList, HashMap

  5. 스레드 적대적 (Thread-hostile) : 모든 method 호출을 외부 동기화로 감싸더라도 Multi-Thread 환경에서 안전하지 않다. 일반적으로 객체간 공유하고 있는 클래스 수준의 정적 데이터를 아무 동기화 없이 수정한다.
    Thread-hostile 으로 밝혀진 class나 method의 경우는 일반적으로 문제를 고쳐 재배포하거나 deprecated API로 지정된다.

  • 조건부 스레드 안전한 클래스의 경우 특히 주의해서 문서화해야 한다고 한다. 일부 method만 동기화 작업이 필요하는데, 어떤 method를 호출할때 외부 동기화 필요한지, 어떤 lock을 얻어야 하는지를 써주어야 한다.

예를 들면 Collcetions.synchronizedMap method에는 다음과 같이 synchronized block안에 들어와야할 변수에 대해 API 문서에 써져있다고 한다.

Read more

Item80.Thread보다는 실행자,태스크,스트림을 애용하라

  • java.util.concurrent 패키지는 Executor Framework (실행자 프레임워크) 라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
1
2
3
4
5
6
7
8
// 백그라운드 작업 큐 생성 ( 하나의 백그라운드 스레드 사용 )
ExecutorService exec = Executors.newSingleThreadExecutor();

// 백그라운드 스레드에게 실행할 작업을 넘기는 method
exec.execute(runnable);

// 실행자 종료
exec.shutdown();

ExecutorService 의 주요 기능들은 다음과 같다.

  1. 특정 task 가 완료되기를 기다린다. (get method)
  2. task 모음 중 아무것 하나 (invokeAny method ) 혹은 모든 task (invokeAll method) 가 완료되기를 기다린다.
  3. ExecutorService가 종료하기를 기다린다(awaitTermination method)
  4. 완료된 task들의 결과를 차례로 받는다 (ExecutorCompletionService 이용)
  5. task를 특정 시간에 혹은 주기적으로 실행하게 한다. (ScheduledThreadPoolExecutor 이용)

Queue를 둘 이상의 Thread 가 처리하게 하고 싶다면 간단히 다른 정적 팩토리를 이용하여, 다른 종류의 실행자 서비스(ThreadPool)를 생성하면 된다.

  • 대부분의 필요한 ThreadPool은 java.util.concurrent.Executors 의 정적 팩토리 method들을 이용해 생성할 수 있다.

  • 작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool 이 일반적으로 좋은 선택이다.

CachedThreadPool 의 동작 방식은 요청받은 task를 queing 하지 않고 바로 thread에게 위임하여 실행시키는 것이다.
가용한 thread가 없으면 새로 하나를 생성한다. 따라서 무거운 서버의 경우에 task요청이 많을떄에는 thread를 계속해서 생성하므로 cpu 이용률이 과도하게 높아진다.

  • 무거운 서버의 경우에는 Thread 개수를 고정한 Executors.newFixedThreadPool이나, 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 휠씬 낫다.
Read more

Item79. 과도한 동기화는 피하라

Alien Method

  • 과도한 동기화는 성능을 떨어트림과 동시에 교착상태(DeadLock) 에 빠트린다.
  • 응답불가와 안전 실패를 피하려면 동기화 method나 동기화 block안에서는 client에게 제어를 양도하면 안된다.
  • 동기화된 영역 안에서는 재정의할 수 있는 method는 호출하면 안되며 client가 넘겨준 함수 객체를 호출해서도 안된다. (동기화된 영역에 예외를 일으키거나, 교착상태에 빠뜨리게 할 수 있다.) 이렇게 client가 넘겨준 함수 객체를 alien method라고 부른다고 한다.

예를 들어보면 아래와 같이 집한을 감싼 wrapper class가 존재하고, 이 wrapper class의 client는 observer 가 추가되면 알림을 받을 수 있는 Observer pattern 이다. 여기서 synchronized block안에서 client로부터 lambda 식 (SetObserver) 을 입력받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ObservableSet<E> extends ForwardingSet<E> {

public ObservableSet(Set<E> set){
super(set);
}

private final List<SetObserver<E>> observers = new ArrayList<>();

public void addObserver(SetObserver<E> observer){
synchronized (observers) {
observers.add(observer);
}
}

public boolean removeObserver(SetObserver<E> observer){
synchronized (observer){
return observers.remove(observer);
}
}

private void notifyElementAdded(E element){
synchronized (observers){
for (SetObserver<E> observer : observers) {
observer.added(this,element);
}
}
}

@Override
public boolean add(E element){
boolean added = super.add(element);
if(added){
notifyElementAdded(element);
}
return added;
}

@Override
public boolean addAll(Collection<? extends E> c){
boolean result = false;
for (E element : c) {
result |= add(element); // notifyElementAdded method를 호출한다.
}
return result;
}

1
2
3
4
5
6
7

@FunctionalInterface
public interface SetObserver<E> {

void added(ObservableSet<E> set , E element);
}

위 class는 다음과 같이 사용될 수 있다.

1
2
3
4
5
6
7
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((s,e)-> System.out.println(e)); // client로부터 함수형 인터페이스의 구현체 (람다)를 입력받는다.
for (int i = 0 ; i < 100 ; i++) {
set.add(i);
}
}

이제 위 로직을 수정해서, 값이 23이면 자기 자신을 제거하는 observer 객체를 추가해보자

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
if(element == 23){
set.removeObserver(this);
}
}
});
}

예상되는 결과는 0~23까지 출력한뒤, 자기 자신은 구독해지를 할 것으로 예상되나 실제로는 ConcurrentModificationException을 던진다.
observer의 added method가 호출된 시점이 notifyElementAdded method가 observer list를 순회하는 도중이기 떄문이다.

다른 형태로 observer가 removeObserver를 직접 호출하지 않고, ExecutorService를 사용해 다른 thread를 사용해 호출해보는 로직을 수정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if(element==23){
ExecutorService exec = Executors.newSingleThreadExecutor();
try{
exec.submit(()->set.removeObserver((this))).get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
finally {
exec.shutdown();
}
}
}
});

위 프로그램을 실행하면 예외 자체는 터지지 않지만 deadlock상태에 빠진다. 백그라운드 thread가 s.removeObserver를 호출하면 observer에 대한 lock을 얻어 잠그려고 시도하지만 main thread가 이미 lock을 가지고 있기 떄문에, lock을 얻을 수가 없는 상태이다. 동시에 main thread는 background thread가 observer를 제거하기만을 기다리고 있다.

위 두 예제는 사실 억지스러운 예제지만 동기화된 영역안에서 client로부터 넘겨받는 코드를 호출하는게 얼마나 프로그램이 오작동하게 만들기 쉬운지를 보여준다.

만약 불변식이 임시로 꺠진 경우라면 Java의 Lock은 재진입을 허용함으로 교착상태에 빠지지 않으므로,다음번 Lock 획득도 성공하고 결과적으로 데이터가 원치 않는 상태로 훼손될 수 있다.

해결 방안

alien method를 동기화 block 바깥으로 옮겨주면 된다고 한다. 이렇게 동기화 영역 바깥에서 호출되는 외계인 메소드를 열린 호출(open call)이라고 부른다고 한다.

Read more

Item81. wait와 notify보다는 동시성 유틸리티를 애용하라

wait 와 notify 에 대한 개념을 정리하기 앞서, java의 Monitor라는 동시성 이슈를 해결하기 위한 동기화 mechanism 이 있다고 한다.

  • Monitor는 한번에 한 thread만 공유자원에 접근할수 있도록 critical section 에 하나의 thread만 존재할 수 있도록 보장해준다. 일종의 lock 개념과 유사하다.

  • Monitor는 Java에서 synchrnoized keyword로 구현되어 있다고 한다.

(reference - https://www.geeksforgeeks.org/difference-between-lock-and-monitor-in-java-concurrency/)

  • wait method : 객체의 monitor를 가지고 있는 thread ( synchronized block 안에 있는 thread)가 실행이 멈춘다. monitor를 가지고 있지 않는 thread가 이 method를 호출하게 되면 IllegalMonitorStateException 을 던진다.

  • notify method : wait하고 있는 thread중에 하나의 thread를 꺠운다.

wait와 notify에 대한 간략한 개념정리는 위와같고, 책에서는 wait와 notify는 올바르게 사용하기 매우 까다로우니 고수준 동시성 유틸리티를 사용하라고 권고하고 있다.

java.util.concurrent 의 고수준 utility는 세 범주로 나눌 수 있는데,

  1. 실행자 framework
  2. 동시성 collection
  3. 동시성 장치 (synchronizer)

동시성 Collection

  • List,Queue,Map과 같은 표준 collection 인터페이스에 동기화 개념을 추가해 구현한 고성능 collection 이다.

  • collection 내부적으로 동기화를 수행한다. 따라서 동기화를 무력화할수없으며, 외부에서 lock을 사용하게 되면 오히려 성능이 느려진다.

  • 동시성 collection에서 동기화를 무력화하지 못하므로 여러 method를 원자적으로 묶어 호출하는 일 역시 불가능하다. 따라서 여러 기본 동작을 하나의 원자적 동작으로 묶는 ‘상태 의존적 수정’ method들이 추가됬다.

상태 의존적 수정 method의 예를 들면 Map의 putIfAbsent(key,value) method는 주어진 키에 매핑된 값이 없을떄만 새 값을 집어넣고 기존값이 있었다면 그 값을 반환하고 없었다면 null을 반환한다. 이 method를 사용해서 thread safe한 map을 쉽게 구현할 수 있다.

1
2
3
4
5
6
public static final ConcurrentMap<String,String> map = new ConcurrentHashMap<>();

public static String intern(String s){
String previousValue = map.putIfAbsent(s,s);
return previousValue == null ? s: previousValue;
}

이를 조금 더 최적화하여 get를 먼저 호출하고 필요할떄만 putIfAbsent method를 호출하면 더 빠르다.

1
2
3
4
5
6
7
8
9
public static String intern(String s){
String result = map.get(s);
if(result == null){
result = map.putIfAbsent(s,s);
if( result == null ){
result = s;
}
}
}
  • 동시성 Collection(ConcurrentHashMap)을 사용하는게 동기화한 Collection (Collections.synchronizedMap) 을 사용하는 것 보다 휠씬 좋다.

  • Collection interface 중에 일부는 작업이 성공적으로 완료될 떄까지 기다리도록 확장되었다. 예를 들면 Queue를 호가장한 BlockingQueue 에 추가된 method중 take는 queue의 첫 원소를 꺼내는데 만약 queue가 비었다면 새로운 원소가 추가될떄까지 기다린다.

이러한 특성 덕분에 BlockingQueue는 작업 큐 (생산자-소비자 queue)에 쓰기에 적합하다고 한다. 실제로 ThreadPoolExecutor를 포함한 대부분의 실행자 서비스 구현체에서 BlockingQueue를 사용한다고 한다.

동기화 장치

  • 동기화 장치는 thread가 다른 thread를 기다릴 수 있게 하여 서로 작업을 조율할 수 있게 해준다. ?

Item78. 공유 중인 가변 데이터는 동기화해 사용하라

synchronzied 키워드는 해당 method나 block을 한번에 한 thread만 수행하도록 보장한다.

동기화를 사용하면 일관된 상태의 객체에 접근하는 method는 그 객체에 lock을 건다.
lock을 건 method는 객체의 상태를 확인하고 필요하면 수정한다. 즉 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다.

동기화에는 중요한 기능이 하나 더 있는데, 동기화 없이는 한 thread가 만든 변화를 다른 thread에서 확인하지 못할 수도 있다는 점이다.
Java는 long 과 double외에 primitive 타입 변수는 원자적(atomic)인데, 이는 여러 thread가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 thread가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 뜻이다.

하지만 주의할점이 있다. 데이터 타입이 원자적이라고 하더라도, 자바 언어 명세는 thread가 field를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만 한 thread가 수정한 값이 다른 thread에게 언제 보이는가는 보장되지 않는다고 한다.

따라서 동기화는 thread간에 배타적인 실행에서 중요할 뿐 아니라, thread간에 안정적인 통신에도 중요하다고 한다.

예를 들어봐서 thread간에 공유중인 가변 데이터를 원자적으로 쓸 수 있다고 하더라도, 동기화를 하지 않으면 다른 thread는 수정이 되기 전의 상태값을 볼수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StopThread {

public static boolean 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); // main thread 1초간 sleep
stopRequested=true; // 공유 가변 데이터의 상태를 바꾼다.
}
}

위 코드를 보면 main thread가 공유 가변 데이터의 상태를 원자적으로 바꾸면 , backgroundThread도 이 변경된 데이터 상태를 보고 종료될 것 같지만 종료되지 않고 계속해서 실행된다.

그 이유는 동기화 떄문인데, 아까 정리한 것처럼 값을 원자적으로 써서, 수정이 완전히 반영된 값을 얻는다고 보장은 하지만 다른 thread가 그 상태를 언제 보게 될지는 모르기 떄문이다.

위 코드는 JVM 가성머신이 다음과 같이 최적화를 수행할 수도 있기 떄문이다.

1
2
3
4
5
6
7
8
9
10
11
12

//원래 코드
while(!stopRequested){
i++;
}

// JVM에 의해 최적화된 코드
while(!stopRequested){
while(true){
i++;
}
}

위 최적화 기법은 openJDK 서버 VM이 실제로 적용하는 hoisting 기법이다.

Read more

Item77. 예외를 무시하지 말자

API 설계자가 메소드 선언에 예외를 명시하는 이유는 메소드를 사용할때 적절한 조치를 취해달라고 요청하는 것이다.
하지만 사용하는 측에서는 다음과 같이 아무 일도 하지 않으면 끝이다.

1
2
3
try{
//
}catch (SomeException e){}

위와 같이 catch block을 비워두면 예외가 존재할 이유가 없어진다.

반대로 예외를 무시해야 할 때도 있는데 예를 들면 FileInputStream을 닫을때 그렇다. 입력 전용 스트림으로 파일의 상태를 변경하지 않았으니 복구할 것이 없으며, 남는 작업을 중단할 이유도 없다.

만약 예외를 무시하기로 결정했다면 catch block안에 그렇게 결정한 이유를 죽석으로 남기고, 예외 변수의 이름도 ignored로 바꿔놓자.

1
2
3
4
5
6
7
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors=4; // 기본값
try{
numColors = f.get(1L,TimeUnit.SECONDS);
}catch(TimeoutException | ExecutionException ignored){
// 기본값을 사용
}
  • 예외를 무시할수 있는 상황이 아닌데, 빈 catch block 을 사용하면 원인도 모른채 프로그램이 죽어 디버깅하기 힘들다. 최소한 throws로 예외를 던지기만 해도 디버깅 정보를 남긴채 프로그램이 중단되게는 할 수 있다.
Read more