자바 병렬프로그래밍[5] 구성 단위

kimji1
10 min readMar 25, 2020

--

반복문이 실행되는 동안 변경 횟수 값이 바뀌면 hasNext 메서드나 next 메서드에서 ConcurrentModificationException 발생

동기화된 컬렉션 클래스는 각 연산을 수행하는 시간 동안 항상 락을 확보하고 있어야 함

Queue 인터페이스

  • 작업할 내용을 순서대로 쌓아둘 수 있는 구조
  • 동기화를 맞추느라 대기 상태에서 기다리는 부분이 없음
  • 큐에서 객체를 뽑아내는 연산을 호출했는데 큐에 뽑아낼 항목이 하나도 들어 있지 않았다면 단순히 null 리턴

ConcurrentLinkedQueue

  • FIFO 큐

PriorityQueue

  • 특정한 우선 순위에 따라 큐에 쌓여 있는 항목이 추출되는 순서가 바뀌는 특성

BlockingQueue

  • 큐에 항목을 추가하거나 뽑아낼 때 ㄷ상황에 따라 대기할 수 있도록 구현
  • 큐가 비어 있다면 큐에서 항목을 뽑아내는 연산은 새로운 항목이 추가될 때까지 대기
  • 큐에 크기가 지정되어 있는 경우 큐가 지정한 크기만큼 가득 차 있다면, 큐에 새로운 항목을 추가하는 연산은 큐에 빈 자리가 생길 때까지 대기
  • BlockingQueue 클래스는 프로듀서-컨슈머(producer-consumer) 패턴을 구현할 때 굉장히 편리하게 사용할 수 있음

ConcurrentHashMap

  • HashMap에서는 모든 연산에서 하나의 락을 사용했기 때문에 특정 시점에 하나의 스레드만이 해당 컬렉션을 사용할 수 있음
  • ConcurrentHashMap은 락스트라이핑(lock striping)이라 부르는 굉장히 세밀한 동기화 방법을 사용해 여러 스레드에서 공유하는 상태에 훨씬 잘 대응할 수 있음
  • 값을 읽어가는 연산은 많은 수의 스레드라도 얼마든지 동시에 처리할 수 있고, 읽기 연산과 쓰기 연산도 동시에 처리할 수 있으며, 쓰기 연산은 제한된 개수만큼 동시에 수행할 수 있음
  • 속도를 보자면 여러 스레드가 동시에 동작하는 환경에서 일반적으로 훨씬 높은 성능 결과를 볼 수 있으며 이와 함께 단일 스레드 환경에서도 성능상의 단점을 찾아볼 수 없음
  • Iterator를 만들 시 즉시 멈춤 대신 미약한 일관성 전략을 취함
  • 미약한 일관성 전략은 반복문과 동시에 컬렉션의 내용을 변경한다 해도 Iterator를 만들었던 시점의 상황대로 반복을 계속 할 수 있음
  • Map을 독점적으로 사용할 수 있도록 막아버리는 기능은 지원하지 않음, Hashtable과 synchronizedMap 메소드를 사용하면 Map 에 대한 락을 잡아 다른 스레드에서 사용하지 못하도록 막을 수 있음

블로킹 큐와 프로듀서-컨슈머 패턴

  • 블로킹 큐는 put과 take라는 핵심 메서드를 갖고 있고, offer와 poll이라는 메서드도 갖고 있음
  • 만약 큐가 가득 차 있다면 put 메서드는 값을 추가할 공간이 생길 때까지 대기
  • 반대로 큐가 비어있는 상태라면 take 메서드는 뽑아낼 수 있는 값이 들어올 때까지 대기
  • 큐는 그 크기를 제한할 수도 있고 제한하지 않을 수도 있기 때문에 put 연산이 대기 상태에 들어가는 일이 발생하지 않음
  • 프로듀서-컨슈머 패턴: ‘해야 할 일’ 목록을 가운데에 두고 작업을 만들어내는 주체와 작업을 처리하는 주체를 분리시키는 설계 방법으로 작업을 만들어내는 부분과 작업을 처리하는 부분을 완전히 분리할 수 있기 때문에 개발 과정을 좀더 명확하게 단순화시킬 수 있고, 작업을 생성하는 뿐과 처리하는 부분이 각각 감당할 수 있는 부하를 조절할 수 있다는 장점이 있음
  • 블로킹 큐를 사용하면 값이 들어올 때까지 take 메서드가 알아서 멈추고 대기하기 때문에 컨슈머 코드를 작성하기 편하다

종류

LinkedBlockingQueue

  • FIFO 형태
  • LinkedList에 대응되며 동기화된 List 인스턴스를 뽑아 사용하는 것 보다 성능이 좋음

ArrayBlockingQueue

  • FIFO 형태
  • ArrayList에 대응되며 동기화된 List 인스턴스를 뽑아 사용하는 것 보다 성능이 좋음

PriorityBlockingQueue

  • FIFO가 아닌 우선 순위를 기준으로 동작하는 큐
  • 항목의 순서를 정렬시켜 사용할 수 있는 여타 컬렉션 클래스와 동일하게 기본 정렬 순서로 정렬시키거나, 아니면 Comparator 인터페이스를 사용해 정렬

SynchronousQueue

  • 큐에 항목이 쌓이지 않으며, 큐 내부에 값을 저장할 수 있도록 공간을 할당하지도 않음
  • 큐에 값을 추가하려는 스레드나 값을 읽어가려는 스레드의 큐를 관리
  • 프로듀서와 컨슈머가 직접 데이터를 주고받을 때까지 대기하기 때문에 프로듀서에서 컨슈머로 데이터가 넘어가는 순간은 굉장히 짧아짐
  • 컨슈머에게 데이터를 직접 넘겨주기 때문에 넘겨준 데이터와 관련되어 컨슈머가 갖고 있는 정보를 프로듀서가 쉽게 넘겨 받을 수 있음
  • 큐에 추가된 데이터를 보관할 공간이 없기 때문에 put 메서드나 take 메서드를 호출하면 호출한 메서드의 상대편 측에 해당하는 메서드를 다른 스레드가 호출할 때까지 대기

직렬 스레드 한정

  • 프로듀서-컨슈머 패턴과 블로킹 큐는 가변 객체를 사용할 때 객체의 소유권을 프로듀서에서 컨슈머로 넘기는 과정에서 직렬 스레드 한정 기법 사용

덱, 작업 가로채기

  • Deque과 BlockingDeque은 각각 Queue와 BlockingQueue를 상속받은 인터페이스
  • Deque은 앞과 뒤 어느 쪽에도 객체를 쉽게 삽입하거나 제거할 수 있도록 준비된 큐이며, Deque를 상속받은 실제 클래스로는 ArrayDeque과 LinkedBlockingDeque이 있음
  • 작업 가로채기(work stealing)이라는 패턴을 적용할 때 덱을 그대로 가져다 사용할 수 있음
  • 프로듀서-컨슈머 패턴에서는 모든 컨슈머가 하나의 큐를 공유해 사용하는데 반해 작업 가로채기 패턴에서는 모든 컨슈머가 각자의 덱을 가짐
  • 특정 컨슈머가 자신의 덱에 있던 작업을 모두 처리하고 나면 다른 컨슈머의 덱에 쌓여있는 작업 가운데 맨 뒤에 추가된 작업을 가로채 가져올 수 있음
  • 작업 가로채기 패턴은 그 특성상 컨슈머가 하나의 큐를 바라보면서 서로 작업을 가져가려고 경쟁하지 않기 때문에 일반적인 프로듀서-컨슈머 패턴보다 규모가 큰 시스템을 구현하기에 적당
  • 컨슈머가 다른 컨슈머의 큐에서 작업을 가져오려 하는 경우에도 앞이 아닌 맨 뒤의 작업을 가져오기 때문에 맨 앞의 작업을 가져가려는 원래 소유자와 경쟁이 일어나지 않음

블로킹 메서드, 인터럽터블 메서드

  • 스레드가 블록되면 동작이 멈춰진 다음 블록된 상태(BLOCKED, WAITING, TIMED_WAITING) 가운데 하나를 갖게 됨
  • 블로킹 연산은 단순히 실행 시간이 오래 걸리는 일반 연산과는 달리 멈춘 상태에서 특정한 신호를 받아야 계속해서 실행할 수 있는 연산
  • 기다리던 외부 신호가 확인되면 스레드의 상태가 다시 RUNNABLE 상태로 넘어가고 다시 시스템 스케줄러를 통해 CPU를 사용할 수 있게 됨
  • 특정 메서드가 InterruptedException을 발생시킬 수 있다는 것은 해당 메서드가 블로킹 메서드라는 의미이고, 만약 메서드에 인터럽트가 걸리면 해당 메서드는 대기 중인 상태에서 풀려나고자 노력
    — InterruptedException을 전달: 받아낸 exception을 그대로 호출한 메서드에 넘겨버림, catch로 잡지 않거나 catch로 받은 뒤 몇 작업을 진행 후 호출한 메서드에 throw하는 방법도 있음
    — 인터럽트를 무시하고 복구: throw 할 수 없는 경우 catch 후 현재 스레드의 interrupt 메서드를 호출해 인터럽트 상태를 설정하고 상위 호출 메서드가 인터럽트 상황이 발생했음을 알 수 있도록 해야 함
  • Thread 클래스는 해당 스레드를 중단시킬 수 있도록 interrupt 메서드를 제공하며, 해당 스레드에 인터럽트가 걸려 중단된 상태인지를 확인할 수 있는 메서드도 있음

동기화 클래스

  • 동기화 클래스: 상태 정보를 사용해 스레드 간의 작업 흐름을 조절할 수 있도록 만들어진 모든 클래스
  • 모든 동기화 클래에 접근하려는 스레드가 어느 경우에 통과하고 어느 경우에 대기하도록 멈추게 해야 하는지를 결정하는 상태 정볼,ㄹ 갖고 있고, 그 상태를 변경할 수 있는 메서드를 제공하고, 동기화 클래스가 특정 상태에 진입할 때까지 효과적으로 대기할 수 있는 메서드 제공

래치(latch)

  • 스스로 터미널 상태에 이를 때까지의 스레드가 동작하는 과정을 늦출 수 있도록 해주는 동기화 클래스
  • 래치가 터미널 상태에 이르기 전에는 어떤 스레드도 통과할 수 없고, 한 번 터미널 상태에 다다르면 모든 스레드가 통과
  • 래치가 한 번 터미널 상태에 다다르면 그 상태를 다시 이전으로 되돌릴 수는 없어 계속 유지

CountDownLatch

  • 래치의 상태는 양의 정수 값으로 카운터를 초기화하며, 이 값은 대기하는 동안 발생해야 하는 이벤트의 건수 의미
  • countDown 메서드는 대기하던 이벤트가 발생했을 때 내부에 갖고 있는 이벤트 카운터를 하나 낮춰주고, await 메서드는 래치 내부의 카운터가 0이 될 때까지, 즉 대기하던 이벤트가 모두 발생했을 때까지 대기하도록 하는 메서드
  • 외부 스레드가 await 메서드를 호출할 때 래치 내부의 카운터가 0보다 큰 값이었다면, await 메서드는 카운터가 0이 되거나, 대기하던 스레드에 인터럽트가 걸리거나, 대기 시간이 길어 타임아웃이 걸릴 때까지 대기

FutureTask

  • Future 인터페이스를 구현하며, Future 인터페이스는 결과를 알려주는 연산 작업을 나타냄
  • FuturTask 가 나타내는 연산 작업은 Callable 인터페이스를 구현하도록 되어 있는데, 시작 전 대기, 시작됨, 종료됨과 같은 세 가지 상태를 가질 수 있음
  • 종료된 상태는 정상적인 종료, 취소, 예외 상황 발생과 같이 연산이 끝나는 모든 종류의 상태를 의미
  • FutureTask가 한 번 종료됨 상태에 이르고 나면 상태는 바뀌지 않음
  • Future.get() 메서드의 동작 모습도 실행 상태에 따라 다른데,
    FutureTask의 작업이 종료됐다면 get 메서드는 그 상태를 즉시 알려주고,
    종료 상태에 이르지 못했다면 get 메서드는 작업이 종료 상태에 이를 때까지 대기하고 종료된 이후에 연산 결과나 예외 상황을 알려줌
  • Callable 인터페이스로 정의되어 있는 작업에서는 예외를 발생시킬 수 있으며, 어디에서든 Error도 발생시킬 수 있음.
  • Callable의 내부 작업에서 어떤 예외를 발생시키건 간에 그 내용은 Future.get 메서드에서 ExecutionException으로 한 번 감싼 다음 다시 throw 함

세마포어(semaphore)

  • 카운팅 세마포어는 특정 자원이나 특정 연산을 동시에 사용하거나 호출할 수 있는 스레드의 수를 제한하고자 할 때 사용
  • 어떤 클래스라도 크기가 제한된 컬렉션 클래스로 활용할 수 있음
  • 해당하는 컬렉션 클래스가 가질 수 있는 최대 크기에 해당하는 숫자로 초기화하고, add 메서드에서 객체를 내부 데이터 구조에 추가하기 전에 acquire 를 호출해 추가할 여유가 있는지 확인

배리어(barrior)

  • 래치를 사용하면 여러 작업을 하나로 묶어 다음 작업으로 진행할 수 있는 관문과 같이 사용할 수 있지만 일회성 객체로 한 번 터미널 상태에 다다르면 다시는 이전 상태를 회복할 수 없음
  • 배리어는 특정 이벤트가 발생할 때까지 여러 개의 스레드를 대기 상태로 잡아둘 수 있다는 측면에서 래치와 비슷하지만 모든 스레드가 배리어 위치에 동시에 이르러야 관문이 열리고 계속해서 실행할 수 있다는 점이 다름
  • 래치는 ‘이벤트’를 기다리기 위한 동기화 클래스이고, 배리어는 ‘다른 스레드'를 기다리기 위한 동기화 클래스
  • 배리어는 대부분 실제 작업은 모두 여러 스레드에서 병렬로 처리하고, 다음 단계로 넘어가기 전에 이번 단계에서 계산해야 할 내용을 모두 취합해야 하는 등의 작업이 많이 일어나는 시뮬레이션 알고리즘에서 유용하게 사용할 수 있음

--

--

kimji1
kimji1

No responses yet