멀티 스레드, 스레드 세이프와 자바

dropKick·2020년 11월 26일
2
post-custom-banner

자바를 넣긴 했지만 굳이 자바에만 통용되는 건 아닌 멀티스레드 문제(?)
실제로 대부분의 상황에서 멀티스레드를 구현 하지는 않는데
이런 상황에서 우리가 중요하게 봐야하는 건 스레드 세이프 문제가 아닐까 싶다.

글은 중간 중간 계속 보충 될 예정


요약

  • 멀티 스레드는 동일 메모리 영역을 공유하며 메모리 공유에 대한 문제가 있다
  • 따라서 멀티 스레드는 스레드 세이프하게 구현되어야 한다
  • 자바는 스레드 세이프를 java.util.concurrent를 통해 지원한다
  • 자바는 스레드 세이프를 Synchronized, volatile, ReentrantLock, AtomicIntger를 통해 지원한다
  • 스레드 세이프의 해결은 또 다른 스레드 문제를 불러올 수 있으므로 정확한 판단에 의해 사용되어야 한다

멀티 스레드

스레드는 언제나 동기화 문제가 따르는데 그렇다면 스레드 동기화엔 어떤 문제가 있고, 자바에서는 어떻게 해결할까?

데이터 공유와 스레드 세이프 문제

데이터 공유

  • 스레드가 별도의 스택을 가지긴 하나 이는 프로세스에 비해 매우 작은 영역이다
  • 따라서 프로세스의 메모리 영역을 모든 스레드가 공유하게 된다
  • 메모리의 접근 즉, 데이터의 접근은 그 무결성과 정합성이 지켜져야 한다
    메모리를 할당하는 연산만 어떤 쓰레드도 접근불가능한 원자적 연산이 가능하다

    JVM에서 64비트 할당은 32비트 2번으로 이루어지기에 원자적이지 않다

따라서 메모리 공유에 의한 데이터 무결성과 정합성을 지키기 위해 안전한 쓰레드의 접근
Thread-Safe가 필요하게 되었다

스레드 문제 해결하기, 스레드 세이프

그렇다면 스레드를 안전하게 사용하기 위해선 어떤 방법을 사용해야할까
뭐 동기화를 유지하거나, 락을 통해 접근을 막거나, 아예 스레드세이프하게 원자적 코드를 구성하거나 여러 방법이 있겠지만 자바에서 스레드 문제를 해결하는 법이니
자바의 java.util.concurrent 패키지가 제공하는 스레드 세이프를 알아본다

스레드 관점의 JVM 메모리 모델

그 전에 메모리 모델을 보면서 왜 자바에서 별도의 패키지까지 제공해가면서
스레드 세이프티를 만들어주려 하는지 간단히 알아본다

하드웨어적 관점의 메모리 모델


별도의 CPU는 CPU 레지스터와 캐시를 소유하고, CPU 내 스레드가 존재한다
멀티 코어는 스레드 세이프에서 상대적으로 자유롭다

JVM 메모리 모델


하지만 JVM의 경우 조금은 다른 메모리 모델을 가지는데 JVM자체가 운영체제에게
일정 메모리 영역을 할당받고, 그 메모리 영역을 애플리케이션 코드가 사용하게 된다는 것이다
따라서 JVM 내 사용자 레벨 스레드들은 별도의 콜스택에 지역변수(Primitive Type)와 메소드가 할당되며, 힙 영역에 모든 객체가 올라가게 된다(Wrapper 포함)


따라서 자바의 메모리 모델에서 객체가 메인 메모리에 할당되었을 경우
캐시에 의해 이미 저장된 값을 가져올 수도 있고 안가져올 수도 있고
각 스레드들이 접근이 어떻게 이루어질지 알 수가 없다
따라서 별도의 동기화, 연산 제한 등의 행동이 없을시엔 데이터의 무결성과 정합성을 보장할 수 없는 문제가 발생하게 된다

Synchronized

private Integer count = 0; 

synchronized(count) { 
	count ++; 
}

Block-Lock으로 크리티컬 섹션(임계영역)을 설정하는 방식
Pessimistic locking(비관적 락)으로 운영체제에서 봐온 임계영역 설정이 이 동기화 방식이다
문제가 발생하지 않더라도 무조건 스레드 차단이 일어나기 때문에 비용이 비싸다

volatile

스레드 세이프를 지원하는 키워드로 쓰기 연산은 무조건 메인 메모리에서 발생한다
캐시 메모리 등에 의한 가시성 문제를 해결할 수 있다

ReentrantLock

Synchronized와 비슷한 스레드 차단 방식이지만 다른점은 재진입이 가능하다는 것
흔히 말하는 뮤텍스에 의한 상호배제 락이 Renntrant Lock 방식으로 다른 프로세스의 스레드가 동일 메소드에 대하여 접근이 가능하다

AtomicInteger

원자적 연산을 보장하기 위한 방법
현대적 프로세서들은 CAS(Compare and Swap)라는 낙관적 락을 대부분 지원한다
비관적 락처럼 모든 스레드의 접근을 차단하기보다 경쟁이 발생할 것 같은 스레드만 비교하고 차단한다

자바의 Collection에서 스레드 세이프를 지원하는 타입들은 대부분 CAS를 통한 원자적 연산을 지원한다

결론

스레드의 메모리 공유에 따라 스레드의 원자성을 보장하기 위한 방법을 사용할 수 있다
하지만 락으로 인한 스레드 간의 경쟁 문제, 경쟁으로 인한 교착 상태 등이 발생가능하다

참조

post-custom-banner

0개의 댓글