안드로이드 개발자의 필독서라 할 수 있겠다.

일반 자바 동시성 프로그래밍에서 부터 안드로이드 비동기 프레임웍까지 두루 다뤄준다.


JAVA8의 Collection#parallelStream을 이용하면 코드양을 획기적으로 줄일 수 있다.

하지만 이전 기술이라고 무시하지 말고 알아두자. 이전에 작성된 코드를 읽을 일도 많이 있을 테니까.




4.4.7 메시지 큐 관찰

메시지 전달을 자세히 살펴보는 방법


1. MessageQueue에 Handler가 추가한 메세지 스냅샷 찍기 

:    handler.dump(new LogPrinter(Log.DEBUG, TAG), "");

(참고) 전달경계를 넘었지만 큐에 남아 있는 메시지는 마이너스 시간으로 표시됨


2. 메시지 큐의 처리 추적

:   Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, TAG));



5.2.2 비동기식 RPC

AIDL 비동기 호출 구현방법

- .aidl 파일의 interface 또는 메소드 선언부에 oneway 키워드를 추가

- 메소드의 인자로 콜백 객체를 받도록 하자


5.3 바인더를 이용한 프로세스간 메시지 전달

: 서버/클라이언트 각각에 생성한 Messenger를 이용한다!



Chap. 8 HandlerThread

mHandler = new Handler(handlerThread.getLooper());

==> 스레드가 시작되었다면 루퍼가 초기화되어 메시지를 받을 수 있을 때까지 block 시킨다. 이렇게 경쟁조건 문제(4.4.1참고) 해결


HandlerThread는 다음의 편리한 속성으로 태스크 연쇄를 위한 구조를 제공한다

- 설정이 용이

- 연쇄로 묶일 수 있는 독립적이고 재사용 가능한 태스크

- 순차적 실행

- 연쇄 안에서 다음 태스크로 연결할지 말지 결정해야 하는 자연(?) 결정 지점

- 현재 상태 보고

- 태스크 연쇄 내 하나의 태스크에서 다른 태스크로의 쉬운 데이터 전달



Chap. 9 Executor 프레임워크를 통한 스레드 실행 제어

안드로이드 비동기 기술의 토대.

AOSP에서 제공하는 비동기 기술로 원하는 실행 동작을 제어하지 못한다면, 실행의 모든 제어를 제공하는 Executor 프레임워크를 사용하자.


9.2.3 스레드 풀 설계

풀의 최대 크기 선정: 

- 너무 적으면? : 충분한 속도로 큐에서 태스크를 꺼내지 못한다. 모든 스레드가 긴 I/O를 연산을 할 경우, I/O 동작이 완료될 때까지 실행 시간을 얻지 못하고 큐에서 기다리는 짧은 수명의 스레드가 있을 수 있다.

- 너무 많으면? : 스레드 전환에 많은 시간을 사용한다.

- 추천개수: N(CPU의 개수) + 1 또는 2*N개

   int N = Runtime.getRuntime().availableProcessors()

- 작업 스레드의 개수보다 많게 하지 말것. 유휴 스레드가 생겨 자원 낭비.

- 같은 스레드 풀에서 서로 종속된 태스크(공통 상태를 공유하거나, 실행 순서가 있을 때)를 실행할 경우, 모든 스레드가 점유되어 있으면 교착상태에 빠질 수 있다. 이를 고려해서 개수를 정해야 함.


9.2.5 스레드 풀의 중단

void shutdown(): 새로운 태스크 거부, 대기중인 태스크는 처리

List<Runnable> shutdownNow(): 새로운 태스크 거부, 대기중 태스크 실행하지 않고 다른 스레드에서 실행할 수 있도록 리스트로 반환


0개의 핵심 풀의 위험

핵심 풀: 스레드 풀에 포함되는 스레드 개수의 하한. 실제로 스레드 풀은 0개 스레드로 시작하지만, 핵심 풀 크기에 도달하면 하한 이하로 떨어지지 않는다.

이를 0으로 지정할 경우, 예를 들어 0개의 핵심 스레드와 10개 태스크를 보유할 수 있는 제한된 큐에서는, 스레드 생성을 싲가하는 11번째 태스크가 삽입될 때까지 어떤 태스크도 실행되지 않는다.


9.3 태스크 관리

Callable과 Future 인터페이스


public interface Callable<V> {

    public <V> call() throws Exeption;

}


ExecutorService#submit를 이용하여 Callable 태스크가 처리되면 Future로 처리한다.


Future에 의해 제공되는 메서드

boolean cancel(boolean mayInterruptIfRunning)

V get() : 결과를 얻을 때까지 기다린다.

V get(long timeout, Timeunit unit): 결과를 얻을 때까지 시간만큼 기다린다.

boolean isCancelled()

boolean isDone()


ExecutorService#invokeAll : 

- 동시에 여러 개의 독립적인 태스크를 실행한다.

- 비동기 계산이 완료되거나 시간제한이  만료될 때까지 스레드 호출을 차단하여 응용프로그램이 모든 태스크가 완료되기를 기다리게 한다. 

- 입력된 순서와 같은 순서로 결과를 Future 리스트에 저장한다.

- 사용예: 두 개의 서로 다른 위치에 있는 네트웍 데이터를 가져와 취합하는 경우


ExecutorService#invokeAny :

- 첫 번째로 마친 태스크에서 결과를 반환한 다음 태스크의 나머지 부분은 무시한다.

- 서로 다른 여러 데이터 집합에 걸쳐 검색하다가 검색 결과가 발견되자마자 즉시 중지하고자 할 경우

- 또는 병렬로 실행되고 있는 태스크들 중에서 하나의 결과만 원할 경우


ExecutorCompletionService :

완료된 결과의 관리. 블로킹 큐를 기반으로 한 '완성 큐'를 가지고 있다.



Chap 10. AsyncTask로 백그라운드 태스크를 UI 스레드에 묶기

AsyncTask는 전역 실행 환경.


AsyncTask#cancel(boolean mayInterrupIfRunning)

: 취소시, 인자에 따라 task 스레드에 인터럽트를 걸 것일지 말 지 결정한다.


상태값: PENDING, RUNNING, FINISHED

           이 값을 이용해서 한 번에 하나의 태스크만 실행할 지 결정할 수 있다.


구현시 메모리 릭이 발생하지 않도록 주의!!

- 6.2절 [스레드 관련 메모리 누수]의 설명과 같이 작업자 스레드가 살아 있는 한 모든 참조 객체는 메모리에 유지된다.

- 따라서 AsyncTask를 독립형(??)이나 정적 내부 클래스로 선언한다. 보통 Context를 참조하는 정적 내부 클래스로 선언한다.


10.3.2 다양한 플랫폼 버전에서 실행

execute의 실행방식은 targetSdkVersion에 달라진다. 문서를 잘 읽어보고 구현할 것!!

executeOnExecutor: 여러 스레드에서 동시 실행할 때 사용. API 11부터 지원.


일관된 동시 실행을 위한 래퍼 클래스

public class ConcurrentAsyncTask {

    public static void execute(AsyncTask as, Object[] params) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) {

            as.execute(params);

        } else {

            as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);

        }

    }

}


10.3.3 커스텀 실행

AsyncTask에 미리 정의된 실행자 (SERIAL_EXECUTOR, THREAD_POOL_EXECUTOR)는 App 전역에 걸쳐 적용되므로, App이 많은 태스크를 실행할 때는 성능 저하의 위험이 있다. 이를 피하려면 Custom Executor로 대체한다.

Executors#newSingleThreadExecutor로 생성해서 인자로 넘기자.


AsyncTaks생성시 매개변수가 없거나(모두 Void),  doInBackground메서드만 구현하는 경우는 단순히 백그라운드로 작업하기 위한 것이므로, HandlerThread를 이용하자


작업자 스레드와 Looper 연결해서 메시지를 전달하길 원한다면 HandlerThread를 사용.


Local Service 내에 백그라운드 스레드를 사용할 때

: Thread, Executor 프레임워크, HandlerThread, 커스텀 실행자를 가진 AsyncTask 중 선택해서 사용



Chap 11. 서비스(를 통한 비동기 실행)


11.6 바운드 서비스

- 지역 바인딩 (11.6.1절)

- Messenger를 이용한 원격 바인딩 (5.3절)

- AIDL을 이용한 원격 바인딩 (5.2절)


11.7 비동기 기술 선정

- 태스크가 순차적으로 실행될 때는 IntentService(12장)를 사용하는 것이 좋다. IntentService는 서비스를 종료시키는 순차적 태스크 실행용 지원을 내장(?? 12장 읽고 문구 수정)하고 있기 때문이다.

- 같은 프로세스에서 다른 component로서 실행되는 AsyncTask는 비전역 실행자 같은 Custom Executor를 사용하거나 Thread pool 처럼 대체 기술을 사용해야 한다.



Chap 12. IntentService

서비스 생명주기의 속성 + 백그라운드 스레드의 태스크 처리를 내장

싱글 백그라운드 스레드에서 태스크를 실행. 즉, 모든 태스크는 순차적으로 실행됨.

11.5.4 태스크 제어 서비스와 같이 활성화된 component를 항상 포함하므로, 너무 일찍 태스크를 종료하는 위험을 줄여준다.

구현과 구조가 이 간단하므로 서비스를 이용하기 전에 먼저 고려해 보자.



사용예1 : 12.2.1. 순차적으로 정렬된 태스크

ex) 웹 서비스 get/post 통신


사용예2 : 12.2.2. 브로드캐스트 리시버에서 비동기 실행

BroadcastReceiver로 앱이 시작될 때 프로세스가 빈 상태(?)라면 런타임이 프로세스를 죽일 수 있다. 그러면 태스크의 결과는 손실된다. 이를 회피할 때 쓰자.

ex) AlarmManager를 이용한 주기적인 작업



Chap 13. AsyncQueryHandler를 이용한 ContentProvider 접근

AsyncQueryHandler: ContentResolver, 백그라운드 실행, 스레드 간 메시지 전달을 처리함으로써 ContentProvider에 대한 비동기 접근을 간소화 해주는 추상 클래스.


Token: 요청 유형(식별자)

Cookie: 요청 식별자 및 임의 객체 유형의 데이터 컨테이너


startQuery, startInsert, startUpdate, startDelete


참고

- ContentProviderOperation

- CancellationSignal: AsyncQueryHandler는 이를 지원하지 않음.  cancelOperation(token)을 사용해야 함.


CursorLoader와 조합하여 사용하는게 이상적이다! (14장 예제 참조)

데이터 쿼리 ==> CursorLoader사용

삽입/업데이트/삭제 ==> AsyncQueryHandler 사용



Chap 14. Loader를 이용한 자동 백그라운드 실행


Loader Framework

: 클라이언트(액티비티 / 프래그먼트)와 함께 동작하는 DB관리 프레임워크

: LoaderManager, Loader(AsyncTaskLoader, CursorLoader)로 구성

: AsyncTaskLoader를 상속받아 CursorLoader대신 커스텀 로더로 많이 사용


로더 프레임워크의 기능

- 비동기 데이터 관리

- 생명주기 관리: 클라이언트가 멈추면 함께 멈춘다. orientation은 변경해도 계속 진행된다

- 데이터 캐시

- 누수 보호: Application Context에서만 작동


LoaderManager#initLoader와 LoaderManager#restartLoader의 차이점

: 클라이언트 상태가 바뀔 때 캐시(로드되어 있는 데이터) 사용 여부


AsyncTaskLoader를 쓰면 비동기로 동작 가능

이는 10.3절의 안드로이드 버전별 동작 차이의 문제가 없다.

저작자 표시 비영리 동일 조건 변경 허락
신고

'요점정리' 카테고리의 다른 글

안드로이드 멀티스레딩(Efficient Android Threading)  (2) 2015.08.05


제목: 자바 8 람다의 힘 / Functional Programming in Java 8

저자: 벤컷 수브라마니암(Venkat Subramaniam)

출판사: 루비페이퍼


함수형 스타일 코드의 큰 이점

1. 변수의 명시적인 변경이나 재할당 문제를 피할 수 있다.

2. 쉽게 병렬화(멀티 쓰레딩)가 가능하다.

3. 서술적인 코드의 작성이 가능하다.

4. 더 '간결'하다. (짧고, 오류가 없고, 개발자의 의도를 효과적으로 전달한다)

5. 직관적이다. 사람이 문제를 설명하는 방식대로 코드를 작성한다.

ex)


함수형 인터페이스: 추상메서드가 하나인 인터페이스

ex) Runnable, Callable, Comparable

유용한 함수형 인터페이스: Consumer<T>, Supplier<T>, Predicate<T>, Function<T, R>







저작자 표시 비영리 변경 금지
신고


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘


하나의 스레드에 작업을 순차적으로 수행한다면?

==> 대부분의 작업은 I/O에 시간이 소요되므로, CPU는 놀게 된다.


작업마다 스레드를 직접 만들어 할당한다면? 

==> 스레드 생성/제거도 공짜가 아니다. 자원과 시간을 소모한다.

==> 생성할 수 있는 스레드 개수는 무한대가 아니다.


따라서 Executor 프레임웍을 사용하자!!


new Thread(runnable).start()

와 같은 코드가 보이면 Executor를 사용해서 유연한 실행 정책을 적용할 것을 고려해야 함.





ThreadPool을 이용할 때의 장점

- 매번 스레드를 새로 생성하지 않고 재활용한다. ==> 자원을 절약, 딜레이가 줄어들어 반응속도 향상. 

- 적절한 스레드 개수 조절로 CPU가 놀지 않도록 함


ThreadPool을 얻는 방법

- newFixedThreadPool

- newCachedThreadExecutor

- newSingleThreadExecutor

- newScheduledThreadPool




ExecutorService 인터페이스에는 동작주기를 관리할 수 있는 여러 메소드가 있다.

- void shutdown()

- List<Runnable> shutdownNow()

- boolean isShutdown()

- boolean isTerminated()

- boolean awaitTermination(long timeout, TimeUnit unit)




Timer 클래스 대신 ScheduledThreadPoolExecutor를 사용하자.

Timer의 단점: 

- 한개의 스레드만을 사용하기 때문에 주기적으로 실행되도록 했을 경우 만약 등록된 작업이 오래 걸린다면 작업이 완료된 후 밀려 있던 작업이 한꺼번에 수행되거나 정책에 따라 누락될 수도 있다.

- Exception 처리를 하지 않는다. Timer 스레드 자체가 멈춘 후 새로운 작업을 등록할 수도 없는 상태가 될 수 있다. 


DelayQueue 사용도 고려해 볼 것.




사용예: (브라우저에서) CompletionService를 이용한 웹페이지 렌더링



저작자 표시 비영리 변경 금지
신고


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘


동기화된 클래스의 대표주자: Vector, Hashtable


문제1. 이 역시 동시에 다른 스레드가 item을 추가/삭제할 경우 - ArrayIndexOutOfException 발생할 수 있음.

- 객체 자체에 synchronized 키워드를 사용하면 되지만 성능이 떨어지게 됨.


문제2. Iterator를 이용한 연산도중 동시성 이슈

- ConcurrentModificationException 발생: 즉시 멈춤(fail-fast)

- 숨겨진 iterator: toString, containAll, removeAll 등의 메소드 내에서 사용됨



병렬 컬렉션

Collections.synchronizedXXX 메소드는 동기화를 우선으로 구현됨. 성능저하.

병렬 컬렉션(java.util.concurrent 패키지에 포함)들은 동기화 보다는 성능을 우선시함. 따라서 연산 도중 size가 바뀔 수 있음. 


- ConcurrentHashMap (putIfAbsent와 같이 미리 구현되어 있는 연산 외 추가 연산이 필요할 경우는 ConcurrentMap을 사용하자)


- CopyOnWriteArrayList: 변경할 때마다 복사. Iterator는 뽑아낸 시점 기준으로 동작. 변경보다 읽기 작업이 많은 경우에 사용.



Blocking Queue (Producer-Consumer 패턴)

작업 생성/처리 부분을 완전히 분리할 수 있다.
- put: 큐가 가득차 있다면 추가할 공간이 생길 때까지 대기한다.
- take: 큐가 비어있는 상태라면 뽑아낼 값이 들어올 때까지 대기한다.
큐와 함께 쓰레드 풀을 사용하는 경우가 Producer-Consumer 패턴을 가장 흔한 경우이다.

- LinkedListBlockingQueue, ArrayBlockingQueue: FIFO 형태의 큐
- PriorityBlockingQueue: 우선순위 기준
- SynchronousBlockingQueue: 큐에 항목이 쌓이지 않는다. Producer는 Consumer에게 직접 작업을 전달해 준다. 따라서 누가 작업을 처리하는 지 알 수 있다. 데이터를 넘겨 받을 수 있는 충분한 개수의 Consumer가 대기하고 있을 경우에 사용하기 좋다.

사용예1. 데스크탑 검색: 파일 인덱싱 서비스
FileCrawler: 디스크에 들어 있는 디렉토리 계층 구조를 따라가면서 검색 댓아 파일이라고 판단되는 파일 이름을 작업 큐에 쌓는다.
Indexer: 작업 큐에 쌓여 있는 파일 이름을 뽑아내어 해당 파일의 내용을 색인한다.

사용예2. 직렬 쓰레드 한정(Serial Thread Confinement)
쓰레드에 한정된 객체는 특정 쓰레드 하나만이 소유권(객체의 내부에 대해 완벽히 알 수 있음)을 가질 수 있다. 
ex) 객체 풀: 객체 풀은 풀 내에 객체가 존재하지 않으면 객체의 상태에 대해 알 수 없다.

사용예3. Deque, 작업 가로채기(Work Stealing)
Producer-Consumer 패턴에서 모든 Consumer가 하나의 큐를 공유하는 것과는 달리 작업 가로채기 패턴은 모든 Consumer가 각자의 deque를 갖는다.
특정 Consumer가 자신의 작업을 모두 처리하면 다른 Consumer의 맨 뒤에 있는 작업을 가져와서 처리한다. 따라서 작업을 가져오기 위해 경쟁이 일어나지 않는다.
Consumer가 Producer의 역할도 갖고 있을 경우에 적용하기 좋다.
ex) Web crawler: 하나의 웹 페이지를 처리하고 나면 링크가 여러가 나타날 수 있다. 이를 deque에 쌓아 둔다.
ex) 가비지 컬렉터: 가비지 컬렉션 도중 힙을 마킹하는 작업


Blocking Method, Interruptable Method
스레드는 여러 가지 원인에 의해 블록 당하거나 멈춰질 수 있다.
- I/O 작업이 치고 들어와서 끝나기를 기다릴 때
- 락이 걸려있는 객체에 접근하려고 할 때
- Thread.sleep 메소드가 끝나기를 기다릴 때

블로킹 메소드: Thread.sleep과 같이 InterruptedException을 발생시키는 메소드. 이를 호출하는 메소드 역시 블로킹 메소드.

InterruptedException 발생시 대처방법
1. InterruptedException을 그대로 전달: 호출한 쪽에 떠넘기기. catch후 사용하던 리소스를 정리하고 나서 throw하는 경우도 있다.
2. 인터럽트를 무시하고 복구: Runnable을 구현한 경우가 해당됨. catch해서 현재 스레드의 interrupt() 메소드를 호출해서 인터럽트 상황을 알린다.




동기화 클래스(Synchronizer)

상태 정보를 사용해 스레드 간의 작업 흐름을 조절할 수 있도록 만들어진 클래스.

ex) Blocking Queue, Latch, FutureTask, Semaphore, Barrier


래치(Latch)

래치가 terminal 상태에 이를 때까지 모든 스레드의 동작을 중단시킨다.

  -. 특정자원을 확보하기 전에는 작업을 시작하지 말아야 할 때

  -. 의존성을 가지고 있는 다른 서비스가 시작하기 전에는 특정 서비스가 실행되지 않도록 막아야 하는 경우

  -. 특정 작업에 필요한 모든 객체가 실행할 준비를 갖출 때까지 기다리는 경우

한 번 terminal 상태에 다다르면 되돌릴 수 없다.


대표적인 래치: CountDownLatch

책의 예제: CountDownLatch를 이용하여 여러 개의 스레드가 동시에 작업을 시작하는 경우 작업을 완료하는 데에 걸리는 시간을 측정


FutureTask

- Executor 프레임웍에서 비동기적인 작업을 할 때,

- 시간이 많이 필요한 작업의 결과를 필요한 시점에 빨리 얻어내기 위해 작업을 미리 시켜 놓을 때 사용


연산작업은 Callable 인터페이스를 구현.

get 메소드는 작업이 종료되었다면 즉시 결과를 리턴, 아니면 종료 상태가 될 때까지 대기.

예외 처리에 주의!


세마포어(Semaphore)

특정 자원을 동시 사용하거나 특정연산을 동시에  호출하는 스레드의 수를 제한하고자 할 때 사용.

ex) 자원 pool이나 컬렉션의 크기에 제한을 두고자 할 때.


생성자에서 permit의 개수를 지정.

acquire 메소드로 permit을 요청하고 허용 가능한 permit이 없을 때는 대기.

release 메소드로 permit을 반납.


이진 세마포어(permit이 1인 경우)는 mutex로 사용 가능.


배리어(Barrier)

실제 작업은 여러 스레드에서 병렬로 처리하고, 다음 단계로 넘어가기 전에 이번 단계에서 계산해야 할 내용을 모두 취합해야 하는 등의 작업이 많이 일어나는 시뮬레이션 알고리즘에서 유용하게 사용 가능.


스레드는 배리어에 도달하면(자신의 작업을 마치면) await를 호출하여 다른 스레드가 배리어에 도달할 때까지 기다린 후 관문을 열고 다음으로 넘어간다.

(래치는 특정 '이벤트'가 일어날 때까지 기다렸다가 작업을 시작한다)

await 호출 후 타이아웃이 걸리거나 대기중 인터럽트가 걸리면 배리어는 깨진 것 으로 간주하고, 대기중인 모든 스레드에 BrokenBarrierException이 발생한다.


CyclicBarrier 클래스를 사용하여 배리어 포인트에서 반복적으로 만나는 기능을 모델링할 수 있다. 커다란 문제를 작은 문제로 분할하여 반복 처리하는 병렬 처리 알고리즘을 구현하고자 할 때 사용.


저작자 표시 비영리 변경 금지
신고


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘


스레드 안전한 객체를 만드는 방법.


4.2 인스턴스 한정

공유 데이터를 객체 내부에 숨겨두고, 이를 관리하는 부분에 락을 적용한다.

ArrayList나 Hashmap은 스레드 안전하지 않지만, Collections.synchronizedList와 같은 API로 생성한 컬렉션은 스레드 안전하다. 이는 래퍼 클래스를 거쳐야만 컬렉션 클래스의 내용을 사용할 수 있기 때문이다.


4.3 스레드 안정성 위임

스레드 안전성이 확보된 필드로만 구성된 클래스는 스레드에 안전할 수도 그렇지 않을 수도 있다.

여러 개의 필드가 서로 독립적(필드들이 서로의 상태 값에 대해 연관성이 없음)이 아닐 경우 문제가 된다.

클래스가 서로 의존성 없이 독립적이고 스레드 안전한 두 개 이상의 클래스(필드)를 조합해 만들어져 있고 두 개 이상의 클래스(필드)를 한번에 처리하는 복합 연산 메소드가 없는 상태라면, 스레드 안정성을 필드에 모두 위임할 수 있다.


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

스레드 안전한 클래스를 확장해서 사용할 경우 스레드 안정성을 확보하는 방법


4.4.1 호출하는 측의 동기화

아래 ListHelper클래스 처럼 list 객체에 락을 걸어야 쓰레드 안전하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public calss ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    ...
    public boolean putIfAbsent(E x) {
        synchronized(list) {
            boolean absent = !list.contains(x);
            if (absent) {
                list.add(x);
            }
            return absent;
        }
    }
}

putIfAbsent 메소드 자체에 synchronized 키워드만 붙인다고 해서 동기화된 list 객체에서 사용하는 lock과 동일한 lock을 사용하는 것이 아니기 때문이다.
자바 API 문서에 따르면 Collections.synchronizedList에서 반화하는 list객체를 이용하여 lock을 사용해야 한다고 명시되어 있다.

4.5 동기화 정책 문서화

적어도 @GuardedBy annotation (Java 1.5부터 지원)을 이용하여 어느 변수가 동기화되는지라도 표기해 두자.


저작자 표시 비영리 변경 금지
신고


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘


3.1.2 단일하지 않은 64비트 연산

64비트를 지원하지 않는 JVM에서 64비트를 사용하는 숫자형(double, long 등)을 사용하는 경우 동시에 여러 스레드가 같은 변수값을 읽고 쓴다면 엉뚱한 값을 가져올 수 있음. 32비트 단위로 메모리에서 fetch/store하기 때문.


3.1.4 volatile 변수

volatile 변수: 메모리 가시성(Memory Visiblity, 한 Thread에서 변경한 특정 메모리의 값이, 다른 Thread에서 제대로 읽어지는지가)을 보장하기 위한 키워드

--> synchronized 블럭으로 처리한 것과 유사하지만, 의미가 명확하지 않아 읽기 어렵고 따라서 오류가 발생할 가능성이 있다.

--> 증가연산자(++)를 사용한 부분은 동기화를 맞춰주지 않는다.


volatile 변수는 다음과 같은 상황에서만 사용하는 것이 좋다.

  • 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나, 해당 변수의 값을 변경하는 쓰레드가 하나만 존재할 때

  • 해당 변수가 객체의 불변조건을 이루는 다른 변수와 달리 불변조건에 관련되어 있지 않을 때

  • 해당 변수를 사용하는 동안에는 어떤 경우라도 락을 걸어 둘 필요가 없는 경우


대표적인 사용예: 간단한 상태 체크

 
올바르게 생성자가 실행된 객체는 다음과 같은 방법으로 안전하게 공개할 수 있다.
  • 객체에 대한 참조를 static 메소드에서 초기화시킨다.
  • 객체에 대한 참조를 volatile 변수 또는 AtomicReference 클래스에 보관
  • 객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관
  • 락을 사용해 올바르게 막혀 있는 변수에 객체 참조를 보관한다.
    ex) Vector나 synchronizedList 메소드 등과 같은 동기화된 컬렉션을 생성

여러 쓰레드를 동시에 사용하는 병렬 프로그램에서 객체를 공유해 사용하고자 할 때 가장 많이 사용되는 몇가지 원칙을 살펴보면 다음과 같다.

쓰레드 한정: 쓰레드에 한정된 객체는 완전하게 해당 쓰레드 내부에 존재하면서도 그 쓰레드에서만 호출해 사용할 수 있다.

읽기 전용 객체를 공유: 읽기 전용 객체를 공유해 사용한다면 동기화 작업을 하지 않더라도 여러 쓰레드에서 언제든지 마음껏 값을 읽어 사용할 수 있다. 물론 읽기 전용이기 때문에 값이 변경될 수는 없다. 불변객체와 결과적으로 불변인 객체가 읽기 전용 객체에 해당한다고 볼 수 있다.

쓰레드에 안전한 객체를 공유: 쓰레드에 안전한 객체는 객체 내부적으로 필수적인 동기화 기능이 만들어져 있기 때문에 외부에서 동기화를 신경쓸 필요가 없고, 여러 쓰레드에서 마음껏 호출해 사용할 수 있다.
동기화 방법 적용: 특정 객체에 동기화 방법을 적용해두면 지정한 락을 획득하기 전에는 해당 객체를 사용할 수 없다. 쓰레드에 안전한 객체 내부에서 사용하는 객체나 공개된 객체 가운데 특정 락을 확보해야 사용할 수 있도록 막혀 있는 객체 등에 동기화 방법이 적용되어 있다고 볼 수 있다.



저작자 표시 비영리 변경 금지
신고


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘


01. 개요

개요 부분은 스레드를 다룰 때 많은 책들에서 언급하는 동시성 문제와 성능 위험에 대해 이야고 있음.


02. 스레드 안정성

만약 여러 스레드가 변경할 수 있는 하나의 상태 변수를 적절한 동기화 없이 접근하면 그 프로그램은 잘못된 것이다. 이를 고치는 세가지 방법.

  • 해당 상태 변수를 스레드 간에 공유하지 않거나
  • 해당 상태 변수를 변경할 수 없도록 만들거나 (불변객체?)
  • 해당 상태 변수에 접근할 땐 언제나 동기화를 사용한다.


스레드 안전한 클래스를 설계할 땐, 바람직한 객체 지향 기법이 왕도다. 캡슐화와 불변 객체를 잘 활용하고, 불변 조건을 명확하게 기술해야 한다.


추상화와 캡슐화 기법이 성능과 배치된다면 성능을 개선하기 전에 코드를 올바르게 작성하는 것이 먼저다. 

최적화는 성능 측정을 해본 이후에 요구 사항에 미달될 때만 하는 편이 좋다.


스레드 안전한 클래스란?

여러 스레드가 클래스에 접근할 때, 실행 환경이 해당 스레드들의 실행을 어떻게 스케줄하든 어디에 끼워 넣든, 호출하는 쪽에서 추가작인 동기화나 다른 조율 없이도 정확하게 동작한다면 해당 클래스는 안전하다고 말한다.

스레드 안전한 클래스는 클라이언트 쪽에서 별도로 동기화할 필요가 없도록 동기화 기능도 캡슐화한다.


2.3 락(Lock)

암묵적인(intrinsic) 락(또는 모니터 락) : 메소드 선언부에 synchronized 키워드를 지정하는 락. 해당 클래스의 인스턴스를 락으로 사용한다.


재진입성(reentrant): 특정 스레드가 자기가 이미 획득한 락을 다시 확보하는 것. 암묵적인 락은 재진입 가능하다. JVM은 락에 대한 소유 스레드와 확보 횟수를 관리한다. 

재진입성이 없다면 다음의 경우 문제가 된다. 부모/자식 클래스 둘다 synchronized 선언된 doSomething이라는 메소드가 있다. 그런데 하위 클래스에서 super.doSomething을 호출할 때 데드락에 빠지게 된다.


2.4 락으로 상태 보호하기

여러 스레드에서 접근할 수 있고 변경 가능한 모든 변수를 대상으로 해당 변수에 접근할 때는 항상 동일한 락을 먼저 확보한 상태여야 한다. 이 경우 해당 변수는 확보된 락에 의해 보호된다고 말한다.

모든 변경할 수 있는 공유 변수는 정확하게 단 하나의 락으로 보호해야 한다. 유지 보수하는 사람이 알 수 있게 어느 락으로 보호하고 있는지를 명확하게 표시하라. 


2.5 활동성과 성능

종종 단순성과 성능이 서로 상충할 때가 있다. 동기화 정책을 구현할 때는 성능을 위해 조급하게 단순성(잠재적으로 안정성을 훼손하면서)을 희생하고픈 유혹을 버려야 한다.

복잡하고 오래 걸리는 계산 적업, 네트웍 작업, 사용자 입출력 작업과 같이 빨리 끝나지 않을 수 있는 작업을 하는 부분에서는 가능한 한 락을 잡지 말아라.







저작자 표시
신고


처음 프로그래밍은 C로 배웠다. 하지만 Java는 내 주 언어다. '프로그램' 모양새를 어느 정도 갖춘 버전을 구현한 언어도 자바이고, 석사 논문 실험할 때의 언어도 자바였다. 지금은 안드로이드로 먹고 살고 있으니 내 생계수단도 자바다.


프로그래밍을 10여년이나 했지만 지금도 멀티 스레드는 두려운 대상이다. 언제 어디서 버그가 튀어 나와 괴롭힐 지 모른다. 더군다나 요즘처럼 코어 개수가 점점 늘어나고 있는 개발환경에서는 더욱 그렇다.


지금이라도 늦지 않았다. 멀티 스레드 개발을 정복해 보자(는 거창한 목표에 도전해 보자). 2008년에 출간된 책이지만 읽을 내용이 풍부하다. 


제목: (멀티 코어를 100% 활용하는) 자바 병렬 프로그래밍 / Java Concurrency in Practice

저자: 브라이어 게츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬

출판사: 에이콘



목차


1장 들어가며 


1부 기본 원리

2장 스레드 안전성

3장 객체 공유

4장 객체 구성

5장 프로그래밍 단위


2부 병렬 프로그램 구조 잡기

6장 작업 실행

7장 중단 및 종료

8장 스레드 풀 활용

9장 GUI 애플리케이션


3부 가용성, 성능, 테스트

10장 가용성을 최대로 높이기

11장 성능, 확장성

12장 병렬 프로그램 테스트


4부 고급 주제

13장 명시적인 락

14장 전용 동기화 기능 구현 

15장 단일 연산 변수와 넌블로킹 동기화

16장 자바 메모리 모델 


부록 A 병렬 프로그램을 위한 어노테이션


자세한 책소개는 ☞ 여기에서




저작자 표시 비영리 변경 금지
신고