발생하는 경우
volitile 변수에 대한 쓰기 작업은 해당 변수를 읽는 모든 스레드에 보이도록 함start() 호출 시 스레드 내 모든 작업은 start() 호출 이후에 실행된 작업 보다 happens-before 관계 성립join()을 호출하면 join대상 스레드의 모든 작업은 join이 반환된 후의 작업보다 happens-before 관계interrupt()를 호출하는 작업이 인터럽트 된 스레드가 인터럽트 감지하는 시점의 작업보다 happens-before 관volatile이라는 키워드를 사용해 메인 메모리에 직접 접근하면 된다.synchronized 키워드를 이용함monitor lock이라고도 부름lock이 필요lock이 없으면 BLOCKED 상태로 대기참고
volatile사용 안해도 synchronized안에 접근하는 변수 메모리 가시성 문제는 해결됨단점
lock을 얻기 위해 BLOCKED 상태가 되면 락을 얻기까지 무한 대기해야함WAITING 상태로 변경 → 누가 깨워주기 전까지 계속 대기(CPU 실행 스케줄링에 들어가지 않음)WAITING 상태의 스레드는 인터럽트를 걸어 중간에 깨울 수 있음BLOCKED상태는 인터럽트로 대기 상태를 빠져나올 수 없음(Synchronized에서만 사용하는 특별한 대기 상태)WAITING, TIME_WAITING상태는 인터럽트가 걸리면 대기 상태를 빠져나옴기능
park() : 스레드를 WAITING상태로 변경parkNanos(nanos) : 스레드를 나노초 동안만 TIMED_WAITING으로 변경 → 이후 RUNNABLE로 변경unpark(thread) : WAITING상태의 대상 스레드를 RUNNABLE로 변경lock구현에 사용되는 lock 인터페이스 → 대표적인 구현체로 ReentrantLock기능
void lock() : lock 획득 / 인터럽트에 응답하지 않음void lockInterruptily() : lock 획득 시도, 다른 스레드가 인터럽트할 수 있음 → 대기 중에 인터럽트가 발생하면 InterruptedException발생, lock획득 포기boolean tryLock() : lock획득 시도, 즉시 성공 여부 반환 → 다른 스레드가 이미 lock을 획득했다면 바로 포기boolean tryLock(long time, TimeUnit unit) : 주어진 시간 동안 락 획득 시도 → 주어진 시간 안에 획득하면 true, 못하면 그냥 false 반환, 포기void unlock() : lock 해제, 대기중인 스레드가 lock 획득Condition newCondition() : Condition 객체 생성, 반환 →private final Lock nonFairLock = new ReentrantLock(); : 비공정 모드 락lock획득 속도가 빠름private final Lock nonFairLock = new ReentrantLock(true); : 공정 모드 락Lock(ReentrantLock)도 2가지 단계의 대기 상태가 존재➡️ Synchronized 대기
lock 획득 대기BLOCKED 상태로 “lock 대기 집합”에서 관리synchronized 를 빠져나갈 때 lock 획득 시도wait() 대기wait() 호출 시 “스레드 대기 집합”에서 대기notify() 호출 시 빠져나감WAITING 상태로 대기➡️ ReentrantLock 대기
ReentrantLock 락 획득 대기lock.lock()을 호출했을 때 lock이 없으면 대기WAITING 상태로 lock 획득 대기lock.unlock()을 호출했을 떄 대기가 풀리며 lock 획득 시도await() 대기condition에서 WAITING 상태로 대기wait()를 호출했을 때 객체 내부의 스레드 대기 집합에서 관리notify() 호출할 때 스레드 대기 집합 빠져나감➡️ 문제 상황(producer-consumer problem / bounded-buffer problem)
⇒ 결국 버퍼 크기가 한정되어 있고, 생산자와 소비자가 함께 생산하고 소비하기 때문에 발생하는 문제
특정 조건이 만족될 때까지 스레드의 작업을 차단(Blocking)
add(), offer(), put(), offer(타임 아웃)take(), poll(타임아웃), remove()ArrayBlockingQueueLinkedBlockingQueue➡️ 멀티 스레드 사용 시 응답성이 중요!
- 대기 상태 → 고객이 중지 요청을 하거나, 너무 오래 대기한 경우 포기하고 빠져나갈 수 있는 방법 필요
- ex) 큐 한계가 1000개일 때, 순간적으로 1000개 넘는 주문이 들어오면 소비가 생산을 따라가지 못하고 가득차게 됨
- 위의 경우 선택지는 4가지가 있음
- 예외 던지기
- 대기 안하고 false 반환
- 대기
- 특정 시간만큼 대기
add(e) : 지정된 요소 큐에 추가 → 큐가 가득 차면 IllegalStateException 예외 던짐remove() : 큐에서 요소 제거, 반환 → 큐가 비어 있으면 NoSuchElementException 예외 던짐element() : 큐 머리 요소 반환, 요소 제거(x) → 큐가 비어 있으면 NoSuchElementExceptionoffer(e) : 지정된 요소 큐에 추가 시도 → 큐가 가득 차면 false 반환poll() : 큐에서 요소 제거, 반환 → 큐가 비면 null 반환peek() : 큐의 머리 요소 반환, 요소 제거(x) → 큐가 비어 있으면 null 반환put(e) : 지정된 요소 큐에 추가할 때까지 대기 → 큐가 가득 차면 공간 생길 때까지 대기take() : 큐에서 요소 제거 후 반환 → 큐가 비어 있으면 요소가 준비될 때까지 대기Examine (관찰) : 해당 사항 없음.offer(e, time, unit): 지정된 요소 큐에 추가 시도, 지정된 시간 동안 큐가 비워지기를 대기 → 시간 초과 시 false 반환poll(time, unit) : 큐에서 요소 제거 후 반환 → 큐에 요소가 없으면 지정된 시간 동안 요소 준비를null 반환i = 1 : 원자적 연산(o)i = i + 1 : 원자적 연산(x)AtomicInteger : 멀티 스레드 환경에서 안전한 증가 연산 수행을 돕는 클래스synchronized 연산과 달리 락을 사용하지 않고 원자적 연산을 만들어 냄SW가 아닌 HW가 제공하는 기능💡 충돌이 많이 없는 경우는 CAS가 빠름
→ ex) 간단한 CPU 연산의 경우
→ 오래 기다리는 작업을 요청할 경우 CPU를 계속 사용하며 기다리게 됨
⇒ 일반적으로 동기화 락을 사용, 특별한 경우에 한해서 CAS 사용해야 함
⇒ 자바의 동시성 라이브러리들은 일반적으로 CAS 연산을 활용함. 우리가 직접 CAS 연산을 활용하는 경우는 드뭄
synchronized, Lock 등과 같은 안전한 임계영역을 만들어 해결 → 코드를 모두 복사해서 synchronized 기능 추가해야 할까? → 성능과 트레이드 오프 발생List<String> list = Collections.synchronizedList(new ArrayList<>());synchronized를 추가하는 프록시 역할 → 동기화 프록시를 만들어냄Collections 가 제공하는 동기화 프록시 기능 덕분에 스레드가 안전한 컬렉션으로 변경해서 사용할 수 있음단점
ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue (생산자, 소비자)List : CopyOnWriteArrayList → ArrayList 대안Set : CopyOnWriteArraySet, ConcurrentSkipListSet (정렬 순서 유지, comparator 사용)Map : ConcurrentHashMap , ConcurrentSkipListMap (정렬 순서 유지, comparator 사용)Queue : ConcurrentLinkedQueueDeque : ConcurrentLinkedDequeBlockingQueueArrayBlockingQueue : 크기 고정 블로킹 큐, 공정 보드 사용(성능 저하)LinkedBlockingQueue : 크기 고정, 무한 블로킹 큐PriorityBlockingQueue : 우선순위 블로킹 큐SynchronousQueue : 중간 큐 없이 생산자, 소비자 직접 거래DelayQueue : 지정된 지연 시간이 지난 후에 소비됨