JMM 메모리 모델을 기반으로 Java 동시 프로그래밍의 핵심 문제: 원자성, 가시성 및 질서성
따라서 그 전에 Java의 JMM 메모리 모델에 대해 이야기해야 합니다. Java 메모리 모델은 메모리를 주 메모리와 작업 메모리로 나누는 Java 가상 머신 사양의 작업 모드입니다. 스레드는 변수를 연산할 때 메모리에 있는 데이터를 작업 메모리에 복사하고, 작업 메모리에서 작업이 완료된 후 다시 주 메모리에 씁니다.
Java 메모리 모델의 기능은 무엇입니까?
스레드가 메모리를 통해 통신하는 방법과 프로그램 정확성을 보장하기 위해 동기화하는 방법을 정의합니다. Java 메모리 모델은 한 스레드에서 읽은 데이터가 다른 스레드에서 쓴 최신 버전임을 규정합니다. 동시성 프로그램에서는 여러 스레드가 동시에 공유변수에 접근할 수 있기 때문에, 동시 스레드 접근으로 인한 예상치 못한 결과를 방지하기 위한 조치를 취하지 않으면 프로그램에 데이터 불일치, 교착상태 등 알 수 없는 다양한 문제가 발생할 수 있으며, Java 메모리 모델은 이러한 문제를 해결하도록 설계되었습니다.
변수 데이터는 메인 메모리에 저장되며, 스레드가 변수를 연산할 때 메인 메모리에 있는 데이터를 작업 메모리에 복사하고, 작업 메모리에서 작업이 완료된 후 다시 메인 메모리에 기록됩니다. .
참고: 여기서 로컬 메모리는 JMM을 추상화한 것에 불과합니다. 가상의 개념입니다. 실제로는 존재하지 않습니다. 메모리 조각을 로컬 메모리라고 합니다. Java 간의 가시성, 질서성 및 원자성 규칙을 설명하는 데 사용되는 방법입니다. 스레드 사양.
1.원자성
Atomic은 분할할 수 없음을 의미하며 일련의 작업을 수행할 때 이러한 작업이 모두 실행되거나 실행되지 않으며 일부만 실행되는 상황이 없음을 의미합니다.
위의 일련의 작업은 구체적으로 무엇을 의미합니까?
그 대답은 공유 변수에 대한 읽기 및 쓰기 작업입니다. 실제로 이는 스레드 간의 경쟁 조건을 처리하고 멀티 스레드 상황에서 공유 변수에 액세스하는 등 전체 동시 프로그래밍의 핵심이기도 합니다.
동시성은 미시적 관점에서 동일한 시간 내에 여러 스레드를 번갈아 실행하는 것을 의미하고 거시적 관점에서 여러 스레드가 함께 실행되는 것을 의미한다는 것은 모두 알고 있습니다. 이는 현재 CPU가 멀티 코어이고 JMM 메모리를 기반으로 하기 때문입니다. 결과적으로 우리는 항상 다음과 같은 문제에 직면합니다: 여러 스레드가 동일한 공유 변수에 대해 동시에 읽기 및 쓰기 작업을 수행합니다 . 어떻게 이러한 작업의 결과를 정확하게 유지할 수 있습니까? 실제로 우리는 이 세 가지를 유지해야 합니다. 속성: 원자성, 가시성, 순서. 이 세 가지 특성을 어떻게 유지하느냐가 이번 동시 프로그래밍 장에서 해결해야 할 문제라고 생각한다.
원자성을 유지하는 이유는 무엇입니까?
여러 스레드가 동시에 동일한 공유 변수에 액세스할 때 원자성이 보장되지 않으면 스레드 안전성 문제가 발생할 수 있기 때문입니다.
예를 들어 여러 스레드가 동시에 카운터 변수를 누적하는 경우 원자성이 보장되지 않으면 계산 결과가 올바르지 않을 수 있습니다. 이러한 상황을 피하기 위해 카운터 변수의 유형을 AtomicInteger로 정의하거나 동기화된 키워드를 사용하여 읽기 및 쓰기 작업을 잠그면 여러 스레드가 카운터 변수에 대해 작동할 때 하나의 스레드만 한 번에 작동하도록 할 수 있습니다. 시간이므로 원자성이 보장됩니다.
원자성을 유지하는 방법은 무엇입니까?
1. 잠금: 동기화 또는 재진입 잠금 사용
공유 리소스를 읽고 쓰는 코드의 일부를 잠급 니다. 한 스레드가 잠금을 획득한 후 다른 스레드가 이 코드에 액세스하려고 하면 다른 스레드가 잠금을 해제할 때까지 스레드가 차단됩니다. 따라서 잠금은 차단 구현입니다. 그리고 이것은 비관적인 자물쇠 아이디어의 실현이다.
2. 동시 작업에 원자 클래스 사용
Java에서는 java.util.concurrent 패키지의 일부 원자 클래스도 제공됩니다. 이는 잠금이 없는 구현이며 낮은 동시성 상황 에서 사용되며 CAS 메커니즘 (Compare-And-Swap) 을 채택합니다 .
즉, Atomic 클래스의 원자성은 휘발성 + CAS를 통해 원자적 연산을 실현합니다. 예를 들어 AtomicInteger 클래스에서 AtomicInteger 클래스의 값은 휘발성 키워드로 수정됩니다. 이는 값의 메모리 가시성을 보장하고 후속 CAS 구현의 기반을 제공합니다.
CAS 메커니즘(더 중요한 점, 인터뷰 빈도가 높음)
CAS 메커니즘(비교 및 교환): 비교 및 교환. 이 알고리즘은 동시 작업을 위한 하드웨어 지원입니다.
낙관적 잠금의 구현 방법으로 스핀 개념을 채택하고 경량 잠금 메커니즘입니다(낙관적 잠금은 잠금 없는 구현입니다).
CAS의 전체 이름은 비교 및 교환이며 이는 낙관적 잠금 아이디어, 즉 잠금 없는 구현입니다. 이 알고리즘은 동시 작업에 대한 하드웨어 지원입니다.
동시성이 낮은 상황에서 사용하는 것이 좋습니다.
주요 아이디어는 스핀 아이디어 입니다. 처음으로 메모리 값 v를 작업 메모리에 가져오고 계산을 수행하여 업데이트된 값 B를 얻은 다음 판단을 내리고 다시 주 메모리에서 V를 꺼내서 다음과 같이 기록합니다. A, A== V인 경우에만 B를 주 메모리로 다시 업데이트할 수 있습니다. 그렇지 않으면 스레드가 이전에 이를 변경했음을 의미하므로 이 변경된 값을 사용하여 지금 프로세스를 반복합니다.
특징:
이는 잠금이 없는 구현입니다.
낮은 동시성 조건에서만 사용할 수 있습니다.
잠금이 없으면 모든 스레드가 공유 데이터에서 작동할 수 있습니다.
잠겨 있지 않기 때문에 차단되지 않으며 잠금보다 더 효율적입니다.
회전적 사고를 채택하십시오.
CAS 메커니즘의 연속 회전(spinlock)은 여러 스레드가 동시에 동일한 변수를 작동하도록 요청할 때 변수 값이 스레드의 기대치를 충족하지 않으면 스레드가 반복적으로 값을 다시 계산하려고 시도함을 의미합니다. 성공할 때까지. , 처리를 포기하거나 잠자기 대신.
CAS 메커니즘을 사용하면 잠금 오버헤드가 없다는 장점이 있으며, 스레드 간 컨텍스트 전환에 따른 오버헤드가 없으므로 잠금 메커니즘을 사용하는 것보다 성능이 높습니다. 그러나 동시에 스핀 대기 증가(예: 너무 많은 스레드)로 인해 문제가 발생하여 성능이 저하될 수도 있습니다. 따라서 CAS 사용 시 적절한 튜닝이 필요합니다.
2. 가시성
가시성은 한 스레드가 수정한 공유 변수의 값을 적시에 다른 스레드에서 볼 수 있음을 의미합니다. 한 스레드가 공유 변수의 값을 수정했는데 다른 스레드가 여전히 이 값을 사용하고 있는 경우, 다른 스레드가 여전히 수정 전의 이전 값을 볼 경우 프로그램 오류가 발생하거나 실행 이상이 발생합니다.
가시성을 유지하는 방법은 무엇입니까?
가시성을 적절하게 확보하는 것은 동시 프로그래밍에서 중요한 주제입니다. 가시성 문제를 해결하기 위해 Java는 다양한 솔루션을 제공합니다.
-
잠금 동기화 메커니즘: 동기화된 키워드 또는 ReentrantLock 클래스와 같은 잠금 동기화 메커니즘을 통해 공유 변수의 읽기 및 쓰기 작업이 원자적이고 가시적이며 순서대로 이루어지도록 할 수 있습니다.
-
휘발성 키워드 : 휘발성 키워드를 사용하면 수정된 변수가 모든 스레드에 표시되고 공유 변수에 대한 읽기 및 쓰기 작업이 원자적으로 이루어지도록 할 수 있습니다.
-
final 키워드: final 키워드로 수정된 변수는 모든 스레드에서 표시되도록 보장되지만 변경 불가능한 변수에만 적용됩니다.
-
원자 변수: Java의 Atomic 패키지는 가시성과 원자성을 보장하면서 기본 데이터 유형에 대한 효율적인 크로스 멀티스레드 작업을 제공합니다.
변수 변경 사항이 즉시 표시되도록 보장하는 휘발성을 사용하면 잠금이 필요하지 않습니까?
휘발성 키워드는 변수의 가시성과 특정 질서를 보장할 수 있지만 잠금 동기화 메커니즘과 동일하지 않으며 잠금 동기화 메커니즘을 대체하는 데 사용할 수 없습니다.
잠금 동기화 메커니즘(예: 동기화된 메서드 또는 코드 블록, ReentrantLock 클래스 등 사용)은 가시성과 질서를 보장할 뿐만 아니라 여러 스레드에 의한 공유 변수에 대한 작업이 원자적임을 보장하여 다른 문제를 방지할 수 있습니다. 휘발성을 사용하여 공유 변수를 수정하면 가시성과 특정 질서만 보장할 수 있지만 복합 작업의 원자성은 보장할 수 없습니다.
즉, 휘발성 키워드는 가시성과 순서 보장을 제공할 수 있지만 잠금 동기화 메커니즘을 대체할 수는 없습니다. 개발자는 스레드 안전을 보장하기 위해 실제 상황에 따라 적절한 동기화 메커니즘을 선택해야 합니다.
3. 질서
질서는 프로그램이 코드의 순서대로 실행된다는 것을 의미합니다.
성능을 최적화하기 위해 때때로 Java는 일부 코드 명령의 실행 순서를 자동으로 조정 하고 재배열하여 속도를 향상시킵니다. 경우에 따라 순서가 조정된 후 후속 코드 작업이 영향을 받을 수 있습니다.
질서를 유지하는 방법은 무엇입니까?
휘발성 키워드로 수정된 변수 의 경우 해당 변수와 관련된 코드는 실행 중에 재정렬되지 않습니다. 장애 문제를 해결할 수 있습니다 .
요약: 휘발성 키워드는 순서와 가시성을 보장할 수 있지만 원자성 보장은 더 복잡합니다. 원자성을 보장하는 방법에는 잠금과 잠금 없음의 두 가지 방법이 있습니다. 공유 변수에 대한 동시 작업에 대해 이 세 가지 속성을 유지하는 한 작업의 정확성을 유지할 수 있습니다. 이것이 Java 동시 프로그래밍의 실제 목적입니다!