반응형
- 병렬 프로그래밍 과정에서 유용하게 사용할 수 있는 자바 패키지에 포함되어 있는 기본 라이브러리들과 몇 가지 디자인 패턴에 대해 알아본다.
- 5.1 동기화된 컬렉션 클래스
5.1.1 동기화된 컬렉션 클래스의 문제점
5.1.2 Iterator와 ConcurrentModificationException
5.1.3 숨겨진 Iterator - 5.2 병렬 컬렉션
5.2.1 ConcurrentHashMap
5.2.2 Map 기반의 또 다른 단일 연산
5.2.3 CopyOnWriteArrayList - 5.3 블로킹 큐와 프로듀서-컨슈머 패턴
5.3.1 예제: 데스크탑 검색
5.3.2 직렬 스레드 한정
5.3.3 덱, 작업 가로채기 - 대표적인 동기화 컬렉션 클래스
Vector, HashTable - JDK1.2 부터는 Collections.synchronizedXxx 메소드로 동기화 클래스 만들 수 있음
모든 public 메소드를 클래스 내부에 캡슐화해 내부값을 한 번에 한 스레드만 사용하도록 제어함 - 기본적으로 스레드 안정성을 확보하고 있음
- 두 개 이상의 연산을 묶어 사용해야 하는 경우에 (ex 반복iteration, 이동 navigation, put if absent) 여러 스레드가 하나의 컬렉션 클래스를 놓고 동시에 내용을 변경하려고 한다면 오동작 할 수 있음.
- 동기화된 클래스의 Iterator를 사용해도 다른 스레드가 컬렉션 클래스 내부 값 변경하는 것은 불가능
대신 fail-fast가 발생한다 (즉시 ConcurrentModificationException 발생)
값 변경 횟수 카운트를 해서 반복문 실행 중 그 값이 바뀌면 예외 발생시킴
(변경 횟수 확인 부분이 동기화 되어있지는 않아 정확치는 않으나 변경 확인 용이므로 괜찮다) - for-each 구문은 compile 시 Iterator 를 사용한 구문으로 변경된다.
for-each 구문이 동기화 되어 있지 않을 경우에는 for-each 구문 실행시 다른 스레드에 의해 추가 삭제가 일어날 수 있다. - 반복문을 실행하는 동안 컬렉션 클래스에 들어 있는 내용에 락을 걸어둔 것과 비슷한 효과를 내려면
clone 메소드로 복사본을 만들어 복사본을 대상으로 반복문을 사용할 수 있다.
최소한 clone 메소드를 실행하는 동안에는 컬렉션의 내용을 변경할 수 없도록 동기화시켜야 한다. - set 변수는 사용시에 동기화되어 있어야 한다.
- System.out.println 에서 set 변수는 내부적으로 toString 메소드를 호출하게된다.
toString 메소드내에서는 Iterator 를 사용하여 Set 내의 Element 에 접근한다.
=> 따라서 해당 부분은 동기화 되어 있지 않다 - toString 뿐만 아니라 hashCode, equals 메소드도 내부적으로 iterator 를 사용한다.
뿐만 아니라 containsAll, removeAll, retainAll 의 메소드도 내부적으로 iterator 를 사용한다. - 위의 경우 동기화된 Set을 쓰면 해결되는 문제이다.
=> 동기화 정책을 클래스 내부에 캡슐화 - 자바 5.0 에서는 HashMap 을 대체할 수 있는 ConcurrentHashMap 을 제공한다.
- CopyOnWriteArrayList 는 Element 의 열람 성능을 최우선으로 구현한 List 객체이다.
- ConcurrentMap 인터페이스에는 put-if-absent, replace, conditional-remove 연산이 추가됐다.
- 기존의 동기화 컬렉션을 병렬 컬렉션으로 교체하는 것만으로 별다른 위험 요소 없이 전체적인 성능을 상당히 끌어 올릴 수 있다.
- BlockingQueue 클래스로 큐에 항목 추가, 뽑아낼 때 상황에 따라 대기할 수 있도록 할 수 있다.
프로듀서-컨슈머 패턴 구현 시 굉장히 편리하게 사용 가능! - 병렬성을 높인 클래스들
SortedMap => ConcurrentSkipListMap
SortedSet => ConcurrentSkipListSet - ConcurrentHashMap 은 lock striping 동기화 방법을 사용한다.
- 값을 읽는 연산은 많은 스레드에서 동시에 처리 가능
- 읽기/쓰기 연산도 동시에 처리 가능
- 쓰기 연산은 제한된 개수만큼 동시 수행 가능
- 멀티 스레드 환경에서 훨씬 높은 성능을 보이고
단일 스레드 환경에서도 성능상 단점은 없다. - Iterator 순회 시 따로 락을 걸지 않아도 된다.
- 반복문 중 내용이 변경되도 Iterator를 만든 시점을 기준으로 처리된다.
- 단, size, isEmpty 메소드 결과값은 추정 값이다.
- HashTable, synchronizedMap 사용에 비해 단점이 있지만 훨씬 많은 장점이 있다.
- ConcurrentHashMap 에는 일반적으로 많이 사용하는
- 없을 경우에만 추가하는 put-if-absent 연산
- 동일한 경우에만 제거하는 remove-if-equal 연산
- 동일한 경우에만 대치하는 replace-if-equal - 구현되어 있지 않는 기능이 필요하면 ConcurrentMap 을 사용하는 것이 낫다
- CopyOnWriteArrayList 클래스는 동기화된 List 클래스보다 병렬성을 훨씬 높이고자 만들어졌다.
- CopyOnWriteArrayList 는 컬렉션의 내용이 변경될 때마다 복사본을 새로 만들어 내는 전략을 취한다.
- Iterator 사용시 변경되는 항목은 복사본에 반영된다.
- 컬렉션의 데이터가 변경될 때마다 복사본을 만들어내는 방법은 컬렉션에 많은 양의 자료가 들어 있다면 성능상의 손실이 크다.
- 따라서 변경 작업보다는 반복문으로 읽어내는 일이 훨씬 빈번한 경우에 효과적이다.
- BlockingQueue 는 put, take 라는 핵심 메소드를 가지고 있다.
put 메소드 실행시 큐가 가득차 있을 경우에는 필요한 공간이 생길때까지 대기한다.
take 메소드 실행시 큐가 비었을 경우에는 값이 들어올때까지 대기한다.
offer 메소드는 큐에 값을 넣으려고 할때 공간이 없으면 추가할 수 없다는 오류를 알려준다. - 블로킹 큐는 애플리케이션이 안정적으로 동작하도록 만들고자 할 때 요긴하게 사용할 수 있는 도구이다.
블로킹 큐를 사용하면 처리할 수 있는 양보다 훨씬 많은 작업이 생겨 부하가 걸리는 상황에서 작업량을 조절해 애플리케이션이 안정적으로 동작하도록 유도할 수 있다. - 프로듀서-컨슈머 패턴 구현 시 유용 (예, 주방 접기 닦기)
- BlockingQueue 의 구현체
- LinkedBlockingQueue
- ArrayBlockingQueue
- PriorityBlockingQueue
- SynchronousQueue - 자바 6.0에서는 Deque, BlockingDeque 가 추가됐다. 둘은 Queue 와 BlockingQueue 를 상속받은 인터페이스이다.
- 작업 가로채기 패턴에서는 모든 컨슈머가 각자의 덱을 갖는다. 만약 특정 컨슈머가 자신의 덱에 들어 있던 작업을 모두 처리하고 나면 다른 컨슈머의 덱에 쌓여있는 작업 가운데 맨 뒤에 추가된 작업을 가로채 가져올 수 있다.
- 컨슈머가 하나의 큐를 보면서 서로 작업을 가져가기 위해 경쟁하지 않으므로,
일반적인 프로듀서-컨슈머 패턴보다 규모가 큰 시스템 구현에 적당함 - 스레드는 여러 가지 원인에 의해서 블록 당하거나 멈춰질 수 있음
- BlockingQueue 인터페이스 put, take 메소드는 InterruptedException을 발생 시킬 수 있다.
(어떤 메소드가 InterruptedException을 발생시킬 수 있다는 것은 블로킹 메소드라는 의미) - Thread#interrupt 메소드 호출하면 스레드를 중단시킬 수 있다.
(stop 메소드는 좋은 방법은 아니다) - interrupt를 호출당하면 interrupt 상태변수값이 true가 되고 이 값을 통해 인터럽트 당했는지 확인해야 하고
적절한 처리를 해줘야한다.
(인터럽트 당했다고 스레드가 알아서 중단되는 것이 아님) - InterruptedException 발생 시 대처 방법 두 가지
1. 예외를 전달
메소드 선언부에 throws를 추가하거나 catch블락에서 잡아 정리 작업 후 throw
2. 무시하고 복구
Runnable 인터페이스 같이 throw할 수 없는 경우에 해당.
catch한 다음 현재 스레드의 interrupt 메소드 호출하여 상위 호출 메소드에 인터럽트 상황이 발생했음을 알린다.
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
public void run() {
try {
processTask(queue.task());
} catch (InterruptedException e) {
// 인터럽트 상태를 유지시킨다
// InterruptedException이 발생하면 인터럽트 상태가 초기화 된다.
Thread.currentThread.interrupt();
}
}
}
catch하고 아무 일도 하지 않는 것은 안된다. 상위 메소드가 인터럽트에 대응할 기회가 사라지게 된다. - 래치
일종이 관문 역할
CountDownLatch 클래스
- countDown 메소드 : 대기하던 이벤트 발생 시 내부 이벤트 카운터 낮춤
- await 메소드 : 래치 내부 카운터가 0이 될 때까지 대기(모든 이벤트가 발생할 때까지 대기) - FutureTask
시간이 많이 필요한 작업이 있을 때 실제 결과가 필요한 시점 이전에 미리 작업을 실행시켜두는 용도
Future인터페이스를 구현했으며, 스레드로 생성하여 실행해서 작업을 시작 시켜두고 get을 통해 작업 결과를 기다리는 식으로 사용한다. - 세마포어
특정 자원이나 특정 연산을 동시에 사용하거나 호출할 수 있는 스레드 수를 제한하고자 할 때 사용
Semaphore 클래스
- acquire 호출하여 퍼밋 확보
- release 호출하여 퍼밋 반납 - 배리어
래치와 유사(but, 래치는 일회성 객체)
래치는 이벤트를 기다리기 위한 것이고, 배리어는 다른 스레드를 기다리기 위한 것이다.
대부분의 실제 작업은 병렬로 처리하고 다음 단계로 넘어가기 전 이번 단계에서 계산해야 할 내용을 모두 취합해야 하는 등의 작업이 일어나는 시뮬레이션 알고리즘에서 유용하게 사용 가능함 - 1 단계 : HashMap 사용
동기화 하여 사용
하지만, 동시에 한 스레드만 접근 가능하여 성능에 문제가 생긴다 - 2 단계 : ConcurrentHashMap 사용
동기화 관련 성능 병목이 개선됌
하지만, 중복 계산될 가능성이 있다 - 3 단계 : ConcurrentHashMap,
FutureTask 사용
동기화 관련 성능 개선
FutureTask 사용 통해 중복 계산 문제를 상당 부분 해결
하지만, 캐싱 여부 확인, 계산, 캐시 저장 부분이 단일 연산이 아니라 여전히 중복 계산 가능성 존재
5장 구성 단위
5.1 동기화된 컬렉션 클래스
5.1.1 동기화된 컬렉션 클래스의 문제점
5.1.2 Iterator와 ConcurrentModificationException
5.1.3 숨겨진 Iterator
5.2 병렬 컬렉션
5.2.1 ConcurrentHashMap
5.2.2 Map 기반의 또 다른 단일 연산
등의 자주 필요한 몇가지의 연산이 이미 구현되어 있음.
5.2.3 CopyOnWriteArrayList
5.3 블로킹 큐와 프로듀서-컨슈머 패턴
5.3.3 덱, 작업 가로채기
작업 가로채기 패턴은 컨슈머가 프로듀서의 역할도 갖고 있는 경우에 적용하기 좋다.
5.4 블로킹 메소드, 인터럽터블 메소드
5.4 블로킹 메소드, 인터럽터블 메소드
5.5 동기화 클래스
5.5 동기화 클래스
5.6 효율적이고 확장성 있는 결과 캐시 구현
4 단계 : ConcurrentHashMap, FutureTask, putIfAbsent 메소드 사용
캐싱 여부 확인, 계산, 캐시 저장 부분이 동기화 되어있지 않아 발생할 수 있는 중복 계산 문제 해결
반응형
'개발 > 병렬 프로그래밍' 카테고리의 다른 글
자바 병렬 프로그래밍 - 7장 중단 및 종료 (0) | 2016.08.29 |
---|---|
자바 병렬 프로그래밍 - 6장 작업실행 (0) | 2016.08.16 |
자바 병렬 프로그래밍 - 4장 객체 구성 (0) | 2016.07.31 |
자바 병렬 프로그래밍 - 3장 객체 공유 (0) | 2016.07.31 |
자바 병렬 프로그래밍 - 2장 스레드 안정성 (0) | 2016.07.14 |