멀티쓰레드 프로그래밍을 하다보면 스레드를 동기화 해주어야 한다. 쓰레드를 동기화 하는 이유는 여러 개의 쓰레드가 같은 프로세스 내의 자원을 공유하면서 작업하는 경우에 서로의 작업이 다른 작업에 영향을 주기 때문이다.
쓰레드의 동기화를 위해서 임계영역(critical section)과 잠금(lock)을 사용한다.
임계영역을 지정하고, 임계영역이 가지고 있는 lock을 단 하나의 스레드에게 빌려주는 개념이다.
lock은 단 하나의 스레드만이 가질 수 있고, 임계영역 내에서 수행할 코드를 수행한 이후에는 lock을 반납하여야한다.
스레드를 동기화하는 방법들
- 임계영역(critical section): 공유 자원에 대해 단 하나의 스레드만 접근하도록 한다. (하나의 프로세스에 속한 스레드만 가능하다)
- 뮤텍스(mutex): 공유 자원에 대해 단 하나의 스레드만 접근하도록 한다. (서로 다른 프로세스에 속한 스레드도 가능)
- 이벤트(event): 특정한 사건의 발생을 다른 스레드에게 알린다.
- 세마포어(semaphore): 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근을 제한한다.
- 대기 가능 타이머(waitable timer): 특정 시간이 되면 대기 중이던 스레드를 깨운다
synchronized를 이용한 동기화
스레드를 동기화 하는 방법 중 가장 간단한 방법은 synchronized 를 이용해서 임계영역을 설정하는 것이다.
- 메서드 전체를 임계 영역으로 설정한다.
- 특정한 영역을 임계 영역으로 설정한다. (객체의 참조변수)
mutex & semaphore
뮤텍스란 MUTual EXclusion. 말 그대로 “상호 배제”로 해석된다.
가장 쉽게 생각하자면 중복 실행 금지 프로그램이라고 생각하면 된다.
윈도우에 프로그램을 하나만 띄워야 하는 프로그램을 만들거나, 실행 되고 있는 프로그램을 다시 실행 했을 경우 실행 중인 해당 프로그램을 보여주거나 이미 실행 되었다는 메세지를 뿌려 주는 식이라고 가정한다면 뮤텍스를 사용하는 것이 좋다. 윈도우 서비스용 프로그램이 대표적인 예라고 할 수 있다.
세마포어는 한 컴퓨터에 사용자가 노트장이라는 프로그램을 최대한 5개만 실행 되도록 개발하고 싶다면 세마포어를 써야 한다. 여기서 최대한 5개만 이라는 5의 수치는 임계 영역(critical section)의 계수이고 이럼 임계영역을 관리하는 것을 계수 세마포어라고 하며 세마포어 중 하나다.
뮤텍스는 상호배제 알고리즘으로 synchronized 로 만든 블록 사이의 로직이 실행이 다 끝날 때까지 락을 걸어 사용한다고 했을 때, 세마포어는 상호배제 알고리즘을 사용하나 거기에 임계영역에 대한 범위를 만들어서 자원을 보호한다고 생각하면 된다.
이진 세마포어에서 임계영역이 0과 1을 갖는 쓰레드나 프로세스는 뮤텍스라고 생각해도 무방하다.
Atomic Access
Atomicity(원자성): 더 쪼갤 수 없는 가장 작은 단위, 프로그래밍에선 모 아니면 도로 처음부터 끝까지 완전히 수행되던가, 아니면 아예 아무것도 아무것도 수행되지 않아야 하는 Action이다. 중간에 멈춰선 안되는 연산이 뭐지?
쇼핑몰에서 물건을 주문할 때 ‘결제’와 ‘상품 수량 변경’은 서로 다른 작업이지만 반드시 한 세트로 진행되어야 한다.
어떤 상품의 재고가 1개밖에 없는데, 해당 상품을 사려는 고객이 2명 존재한다.
- 1번 회원 결제 성공
- (상품 수량 업데이트가 아직 안 끝났는데) 2번 회원도 결제 성공
- 상품 수량 변경 (재고 0개)
- 상품 수량 변경 (2번 회원은 이미 돈을 지불했는데 재고가 없다)
이 작업이 정상적으로 수행되려면 1번 회원 결제 결과가 재고 수량에 반영되기 전 2번 회원의 결제 시도는 잠시 미뤄놔야 한다. 그리고 2번 회원의 작업이 시작되려 할 때 재고를 확인하고 품절이 되었다면 아예 결제부터 불가능하게 막아놔야 한다.
이 외에도 ‘수강 신청 → 출석부 갱신’, ‘결제 → 좌석 예약’ 처럼 실행 결과가 다른 작업에 영향을 줄 수 있는 작업이 일부만 수행될 경우 데이터에 결함이 생길 수 있다.
여러 개의 작업을 쪼개서 번갈아기며 실행하는 멀티 쓰레드 환경에서 비 원자 연산이 돌아가면 위와 같은 문제가 생길 수 있다. 이처럼 작업 단위가 분리되면 안되는 연산에 Atomic operation이 필요하다.
Atomic Type
Atomic Type 은 단일 변수에 대해서 Atomic Operations을 지원한다.
Wrapping 클래스의 일종으로, 참조 타입과 원시 타입 두 종류의 변수에 모두 적용이 가능하다. 사용 시 내부적으로 Compare-And-Swap(CAS) 알고리즘을 사용해 lock 없이 동시화 처리를 할 수 있다.
주요 Class
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicIntegerArray
- AtomicDoubleArray
주요 Method
- get() : 현재 값을 반환한다.
- set(newValue) : newValue로 값을 업데이트한다.
- getAndSet(newValue) : 원자적으로 값을 업데이트하고 원래의 값을 반환한다.
- compareAndSet(expect, update) : 현재 값이 예상되는 값(expect)과 동일하다면 값을 update 한 후 true를 반환한다. 예상하는 값과 같이 않다면 update는 생략하고 false를 반환한다.
- Number 타입의 경우 값의 연산을 할 수 있도록 addAndGet(delta), getAndAdd(delta) , getAndDecrement(), getAndIncrement(), incrementAndGet() 등의 메서드를 추가로 제공한다.
Volatile
- volatile keyword는 Java 변수를 Main Memory에 저장하겠다라는 것을 명시한다.
- 매번 변수의 값을 Read 할 때마다 CPU cache에 저장된 값이 아닌 Main Memory에서 읽는 것이다.
- 또한 변수의 값을 Write 할 때마다 Main Memory에 까지 작성하는 것이다.
- volatile 변수를 사용하고 있지 않은 MultiThread 어플리케이션에서는 Task를 수행하는 동안 성능 향상을 위해 Main Memory에서 읽은 변수 값을 CPU Cache에 저장하게 된다.
- 만약에 MultiThread 환경에서 Thread가 변수 값을 읽어 올 때 각각의 CPU Cache에 저장된 값이 다르기 때문에 변수 값 불일치 문제가 발생하게 된다.
RenntrantLock VS synchronized
- synchronized의 경우 Thread간의 lock을 획득하는 순서를 보장해주지 않음
이것을 unFair라고 하고, ReentrantLock은 Fair, unFair 모두 지원
- 코드가 단일 블록의 형태를 넘어 여러가지 컬랙션이 얽혀 있을때 명시적으로 락을 실행시킬 수 있음
- 대기상태의 락에 대한 인터럽트를 걸어야 할 경우
- 락을 획득하려고 대기중인 스레드들의 상태를 받아야 할 경우에 쓸 수 있음
ReentrantLock 쓸 경우
- 락을 모니터링 해야 할 때
- 락을 획득하려는 쓰레드의 개수가 많을 때(4개 이상)
- 위에서 이야기한 복잡한 동기화 코드를 작성해야 할 때
단점
- 기본 키워드인 synchronized과 달리 java.util.concurrent를 import해야 되고 try/fianlly block이 무조건 들어가기 때문에 코드가 지저분
CountDownLatch
'자바 & 스프링' 카테고리의 다른 글
PreparedStatement 쿼리를 사용하는 이유 (0) | 2021.11.30 |
---|---|
JPA allocationSize default 값이 50인 이유 (0) | 2021.11.30 |
java - Stream (0) | 2021.11.30 |
Java 8 Lambda — 2 (0) | 2021.11.30 |
Java 8 Lambda — 1 (0) | 2021.11.30 |