Seven Concurrency - Thread & Lock
상호 배제와 메모리 모델
병렬성은 여러 일을 한꺼번에 실행하는 데 관한 것이다.
동시성은 여러 일을 한꺼번에 다루는 데 관한 것이다.
동시성 > 병렬성 : 동시성이 더 큰 개념인가??!!
동시성과 병렬성
- 멀티쓰레드가 각각 하나의 일을 처리해서 여러 일을 하는 것 : 병렬성
- 단일쓰레드가 여러 일을 하는 것 : 동시성
- 멀티쓰레드가 여러 X 여러 일을 하는 것 : 병렬성 + 동시성
사용할 수 있는 것은 synchronized(에이싱크로나이즈드)와 volatile(봘라타일) 뿐
1일차 정리
- 공유되는 변수에 대한 접근을 반드시 동기화(synchronized)한다.
- 쓰는 스레드와 읽는 스레드가 모두 동기화되어야 한다.
- 여러 개의 잠금장치를 미리 정해진 공통의 순서에 따라 요청한다.
- 잠금장치를 가진 상태에서 외부 메서드를 호출하지 않는다.
- 잠금장치는 최대한 짧게 보유한다.
내제된 잠금장치를 넘어서
Lock, ReentrantLock
synchronized 비해 신규 추가된 Lock를 이용하면 잠금을 중간에 풀 수도 있고 잠금에 timeout도 지정할 수 있어서 더 낫다.
tryLock(timeout, timeunit)과 같은 방식으로 즉 timeout을 지정해서 데드락를 빠져나가는 방식은 그다지 추천되지는 않는다.
애초에 timeout 대기 시간동안 다른 스레디 들이 대기 상태가 걸리는 것이고, 데드락을 완전히 회피할 순 없다.
또한 라이브락 상황에서 자유롭지 못하다. 다른 완화책들(timeout을 다르게 설정)이 있긴하지만
근본적으로는 데드락을 발생시키지 않게 하는 것 이 더 중요하다.
조건변수
어떤 일을 하기 위해서 조건을 기다리는 것
예를 들면 큐에 담긴 값을 제거하려고 하는 경우 적어도 큐가 하나의 값을 가질 때까지 기다리는 경우, 혹은 버터에 값을 넣을려고 할 때 공간이 생길 때까지 기다리는 경우
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!/* 조건이 참이다 */)
condition.await();
// 공유되는 자원 사용
} finally {
lock.unlock();
}원자변수
AtomicInteger
자체적으로 동시성 이슈를 해결해줌. 말 그대로 메서드가 원자적임 또한 non-blocking(lock-free) 알고리즘 기반이기 때문에 데드락도 없음
volatile
이 키워드를 사용하면 해당 변수를 읽거나 쓰는 동작에 대해 명령어 순서가 뒤바뀌지 않는다.
그저 변수를 쓰거나 읽은 행위의 순서와 원자성만 보장할 뿐이다. 가능하면 volatile를 사용해서 직접 동시성을 제어하기 보다는 java.util.concurrent 를 활용하라
2일차 정리
- 잠금장치를 얻고자 기다리는 과정을 가로챌 수 있다.
- 잠금장치를 기다리는 동안 타임아웃이 발생할 수 있다.
- 잠금장치를 얻고 반납하는 동작이 임의의 순서로 일어날 수 있다.
- 임의의 조건이 참이 되는 것을 기다리기 위해 조건 변수를 사용할 수 있다.
- 원자 변수를 이용해서 잠금장치를 사용하는 것을 피할 수 있다.
2일차 자율학습
ReenterantLock은 공정성(fairness)을 지원한다. 잠금장치가 “공정하다”는 것은 무엇을 믜미하는 것인가? 공정한 잠금장치를 이용해야하는 이유와 이용하지 않아야하는 이유는 무엇인가?- 공정성 옵션이 활성화 되면 잠금시간이 긴 것이 우선 실행된다. 그리고 공정하지 않는 잠금장치를 이용하면 성능이 더 나아진다.
ReenterantReadWriteLock은 무엇인가? 그것은 ReentrantLock과 어떻게 다른가? 그것은 언제 사용하면 좋은가?- 읽기와 쓰기 별로 락을 분리한 것. 일반적으로 쓰기보다는 읽기가 많은 상황이 많은데 그럴 때 더 유리
- 불필요한 기상(spurious wakeup)이란 무엇인가? 그런 일은 언제 일어나며, 좋은 코드에서는 그런 일이 일어나지 않아야 하는 이유는 무엇인가?
- 조건 변수가 만족하지 않았어도 스레드는 깨어나서 조건이 만족되지 않음을 확인하고 다시 잠드는 것을 말하는 것
- 위와 같은 불필요한 기상이 잦으면 로직이 실행할 수 없음에도 지속적으로 CPU를 소모하게 되므로 성능에 좋지 않음. 그래서 발생하지 않도록 해야함
AtomicIntegerFieldUpdater는 무엇인가? 그것은AtomicInteger와 어떻게 다른가? 그것은 언제 사용하면 좋은가?- 리플렉션 기반으로 volatile 을 가지는 변수를 원자화 시키는 유틸리티
- 필드 직접 접근이 힘들 때 사용하면 좋을 것 같음
거인의 어깨 위에서
굳이 병행성 코드를 만들지 말고 java.util.concurrent 를 사용하자.
CopyOnWrite
항상 방어복사를 하는 것이 아니라 변경(Write)될 때만 복사를 해서 동시성 이슈 제거
완전한 프로그램
자료구조를 이용할 경우 직접 동기화 코드를 만들지 말고 java.util.concurrent 내 스레드에 안전한 자료구조들을 사용하자.
ConcurrentHashMap를 사용할 경우 put 대신 원자적인 putIfAbsent, replace를 잘 이용하자.
3일차 정리
- 스레드를 직접 만드신 대신 스레드 풀을 이용한다.
CopyOnWriteArrayList를 이용해 리스너 관리를 더 쉽고 효과적으로 만든다.- 생산자와 소비자가
ArrayBlockingQueue를 이용해 더 효과적으로 의사소통을 한다. ConcurrentHashMap을 이용해 맵에 대한 동시적인 접근을 지원한다.
3일차 자율학습
ForkJoinPool에 대한 문서. fork/join 풀은 일반적인 스레드 풀과 어떻게 다른가? 어느 하나를 선호해야하는 이유나 상황은 무엇인가?- 작업을 가능한 잘게 쪼개서(재귀적으로) 가능한 병렬처리를 극대화 시키기 위함
- 작업 훔치기(work stealing)는 무엇이고 어떤 도움을 주는가?
java.util.concurrent가 제공하는 도구를 이용해 작업 훔치기를 어떻게 구현하는가?- 프로세서의 작업이 종료되면 다른 프로세서의 대기열을보고 작업 항목을 “훔치기(도용)”합니다. 결과적으로, 작업 훔치기는 유휴 프로세서에 대한 스케줄링 작업을 분배하여 프로세서 사용 효율을 극대화 하는 것입니다.
ForkJoinPool를 이용하면 구현.
CountDownLatch와CyclicBarrier는 어떤 차이가 있는가? 어느 하나를 선택해서 사용하는 이유나 상황은 무엇인가?- 암달의 법칙(Amdahl’s law)은 무엇인가? 우리의 단어 세기 알고리즘 성능이 개선될 수 있는 이론적인 한계에 대해서 이 법칙은 무엇을 이야기해주는가?
- https://namu.wiki/w/%EC%95%94%EB%8B%AC%EC%9D%98%20%EB%B2%95%EC%B9%99
- 병렬화가 불가능한 작업 때문에 성능 상승에 한계가 있다.