공유자원이란?
여러 쓰레드가 동시에 접근할 수 있는 자원을 의미한다.
임계영역이란?
공유자원들 중 여러 쓰레드가 동시에 접근했을 때 문제(경쟁상태)가 생길 수 있는 부분을 말한다.
공유자원 ) 임계영역
경쟁상태란?
둘 이상의 쓰레드가 공유자원을 병행적으로 읽거나 쓰는 동작을 할 때 타이밍이나 접근 순서에 따라 실행 결과가 달라지는 상황이다.
문제 상황 예시)
Read - Modify - Write
Thread 1) count = 0 [read] -> count = 1 [modify] -> count = 1 [write]
Thread 2) --------------------- -> count = 0 [read] -> count = 1 [modify] -> count = 1 [write]
이러한 상황에서 각 쓰레드에서 더해진 count는 제대로 반영되지 않을 수 있다.
쓰레드 1의 write가 끝났을 때는 count가 1이다.
쓰레드 2가 하고 싶었던 행위는 count++인데 결과적으로 count = 1을 한번 더 쓰고 있다.
read할 당시에 쓰레드1이 write하기 전이었기 때문이다.
Check - then - act
Thread 1) count = 3 [check] -> if(count == 3) [then] -> print count [act]
Thread 2) ----------------------------------- count += 100
쓰레드 1에서 마지막에 count를 print했을 때 몇이 나올까?
코드를 짠 개발자는 count = 3이 출력될 것을 생각하고 짰을 것이다.
하지만 실제로 출력되는 값은 저 두 쓰레드만 있다고 가정해도 103이다.
실제로는 더 많은 쓰레드를 운영할테니 결과값을 더욱 예상하기 힘들다.
그렇다면 이와 같은 경쟁상황을 해결하기 위해서는 어떻게 해야할까?
원자성과 가시성이 보장되면 된다.
원자성이란?
공유자원에 대한 작업의 단위가 더이상 쪼갤 수 없는 하나의 연산인 것처럼 동작하는 것.
즉, 한 쓰레드가 연산을 하고 있을 때 다른 쓰레드가 접근하지 못하게 하면 원자성 보장이 가능하다.
가시성이란?
쓰레드가 실행될 때에 CPU는 Main Memory에 있는 값을 불러와서 쓴다.
하지만 CPU와 Main Memory의 거리가 멀어 CPU는 쓰레드가 실행될 때에 필요한 값을 메인 메모리에서 불러 CPU 캐시라는 곳에 저장해 사용한다.
이 후 각 쓰레드에서 작업이 완료되면 해당 CPU 캐시에 있던 값을 메인 메모리에 반영하게 되는데
다른 CPU 캐시에서는 이 값이 현행화되지 않는다면 가시성이 지켜지지 않은 것이다.
가시성을 지키는 방법에는 읽어가는 값에 volatile 를 앞에 붙여
CPU 캐시를 사용하지 않고 메인 메모리의 값을 바로 읽고 쓰게하는 방법이 있다.
동기화란?
원자성과 가시성을 지키는 것.
동기화의 방식
블로킹 방식이란?
특정 쓰레드가 작업을 수행하는 동안 다른 작업은 진행하지 않고 대기하는 방식
Monitor, Synchronized 키워드가 예시로 있다.
Synchronized: 연산결과가 메모리에 써질 때까지 다른 쓰레드는 대기
블로킹 방식에 대한 단점
나머지 쓰레드는 대기 하기에 성능 저하 발생
락 방식을 사용하기 때문에 데드락 발생 가능
간단하게 데드락이란?
각 쓰레드가 서로서로가 필요로 하는 자원을 점유, 락하고 있어서 무한정 서로 대기하는 상태
논블로킹 방식
다른 쓰레드 작업과 상관없이 자신의 작업을 수행하는 방식
컨텍스트 스위칭이 필요가 없다.
Atomic 타입이 예시이다.
CAS 알고리즘 (Compare And Set)
- 기존값
- 변경할 값
- 메모리가 가지고 있는 값
기존값이 메모리가 가지고 있는 값과 같다면 (다른 쓰레드가 데이터를 업데이트하지 않았다면)
변경할 값을 메모리에 반영하며 true를 반환한다.
역의 경우 반영하지 않고 false를 반환한다.
개발자는 false값이 나왔을 때 무한 루프를 돌며 true가 나올 때까지 로직을 수행하는 방식과 몇 번 시도하다 예외를 터트리는 방식 중 선택하곤 한다.
무한루프를 돌며 true값을 기다려도 블로킹 방식보다 효율적이다.
만약 쓰레드가 100개가 있다고 가정을 했을때 한 쓰레드가 자원을 점유하고 있으면 나머지 99개 자원은 전부 Blocked로 컨텍스트 스위칭을 해야 한다.
또한, 1개가 끝난 후에도 하나를 Running으로 바꿔야하는 컨텍스트 스위칭(CPU 많이 소모)이 일어나서 비효율적이다.
무한루프를 돌며 true를 기다려도 true가 나오는 순간 컨텍스트 스위칭 없이 값이 반영이 가능하다는 점, 병렬 쓰레드가 각각 모두 일하고 있다는 점에서 블로킹보다 훨씬 효율적으로 보인다.
그래서 Atomic 타입이란?
CAS 알고리즘 + Volatile = Atomic
동시성을 보장하기 위해 자바 1.8에서부터 제공하는 Wrapper 클래스이다.
Atomic 클래스 내부의 값을 불러오는 변수는 Volatile로 선언이 되어있다.
따라서 JVM에서 CPU로 캐시를 거치지 않은 채 바로 값을 불러온다. -> 가시성 보장
연산을 수행할때는 compareAndSet()이라는 메소드가 호출이 된다. -> CAS 알고리즘을 수행해 원자성 보장.
쓰레드 안전한 객체란?
여러 쓰레드가 동시에 클래스를 사용하려 하는 상황에서 클래스 내부의 값을 안정적인 상태로 유지할 수 있는 객체이다.
'Language > Java' 카테고리의 다른 글
OCP 원칙이란? (0) | 2024.06.25 |
---|---|
[Java] 동시성 문제, 쓰레드 로컬 ThreadLocal (1) | 2023.11.30 |
Java 기본 - Wrapper Class 래퍼 클래스 (0) | 2023.06.30 |
Java 기본 - 접근자 (0) | 2023.06.30 |
Java 기본 - 오버라이딩 오버로딩 (0) | 2023.06.29 |