>Java >Java베이스 >Java CAS 원리 분석 소개

Java CAS 원리 분석 소개

coldplay.xixi
coldplay.xixi앞으로
2020-12-24 17:37:002581검색

java 기본 튜토리얼열에서는 Java CAS 분석을 소개합니다

Java CAS 원리 분석 소개

권장(무료): java 기본 튜토리얼

1. CAS 풀네임 비교 입니다 스왑(swap)은 멀티 스레드 환경에서 동기화 기능을 구현하는 데 사용되는 메커니즘입니다. CAS 연산에는 메모리 위치, 예상 값, 새 값이라는 세 가지 피연산자가 포함됩니다. CAS의 구현 논리는 메모리 위치의 값을 예상 값과 비교하는 것입니다. 두 값이 같으면 메모리 위치의 값을 새 값으로 바꿉니다. 같지 않으면 작업이 수행되지 않습니다.

Java에서는 CAS를 직접 구현하지 않습니다. CAS 관련 구현은 C++ 인라인 어셈블리 형태로 구현됩니다. Java 코드는 JNI를 통해 호출되어야 합니다. 구현 세부 사항은 3장에서 분석하겠습니다.

앞서 말씀드린 것처럼 CAS 운영 과정은 어렵지 않습니다. 하지만 위의 설명만으로는 충분하지 않습니다. 다음으로 다른 배경 지식을 소개하겠습니다. 이러한 배경 지식이 있어야만 후속 내용을 더 잘 이해할 수 있습니다.

2. 배경 소개

우리는 CPU가 버스와 메모리를 통해 데이터를 전송한다는 것을 모두 알고 있습니다. 멀티 코어 시대에는 여러 코어가 동일한 버스를 통해 메모리 및 기타 하드웨어와 통신합니다. 아래와 같이:

Java CAS 원리 분석 소개사진 출처: "컴퓨터 시스템 심층 이해"

위 그림은 비교적 간단한 컴퓨터 구조 다이어그램이지만, 문제를 설명하기에 충분합니다. 위 다이어그램에서 CPU는 두 개의 파란색 화살표로 표시된 버스를 통해 메모리와 통신합니다. CPU의 여러 코어가 동시에 동일한 메모리에서 작동하는 경우 이를 제어하지 않으면 어떤 오류가 발생합니까? 여기에 간략한 설명이 있는데, 코어 1이 32비트 대역폭 버스를 통해 메모리에 64비트 데이터를 쓴다고 가정하면, 코어 1은 전체 작업을 완료하기 위해 두 번 써야 합니다. 코어 1이 처음으로 32비트 데이터를 쓴 후 코어 2는 코어 1이 쓴 메모리 위치에서 64비트 데이터를 읽습니다. 코어 1은 모든 64비트 데이터를 메모리에 완전히 기록하지 않았으므로 코어 2는 이 메모리 위치에서 데이터를 읽기 시작하므로 읽은 데이터는 혼란스러울 것입니다.

하지만 실제로는 이 문제에 대해 걱정할 필요가 없습니다. 인텔 개발자 매뉴얼을 통해 우리는 펜티엄 프로세서부터 인텔 프로세서가 64비트 경계에 정렬된 쿼드워드의 원자적 읽기 및 쓰기를 보장한다는 것을 알 수 있습니다.

위 설명을 바탕으로 Intel 프로세서가 단일 액세스 메모리 정렬 명령이 원자적으로 실행되도록 보장할 수 있다는 결론을 내릴 수 있습니다. 하지만 메모리에 두 번 액세스하라는 명령이라면 어떻게 될까요? 대답은 보장되지 않습니다. 예를 들어, 증가 명령어

에는 두 번의 메모리 액세스가 포함됩니다. 값 1이 메모리의 지정된 위치에 저장되는 상황을 생각해 보세요. 이제 두 CPU 코어가 동시에 명령을 실행합니다. 두 코어가 번갈아 실행되는 과정은 다음과 같습니다.

inc dword ptr [...],等价于DEST = DEST + 1。该指令包含三个操作读->改->写

코어 1은 메모리의 지정된 위치에서 값 1을 읽어 레지스터에 로드합니다.
  1. 코어 2는 메모리의 지정된 위치에서 값 1을 읽습니다.
  2. Core 1 레지스터의 값을 1
  3. Core 2 레지스터의 값을 1
  4. Core 1 감소시킵니다. 수정된 값을 다시 메모리에 씁니다
  5. Core 2 수정된 값을 씁니다. back to the memory
  6. 위 과정을 실행한 후, 메모리의 최종 값은 2인데, 3을 예상하고 있었으니 문제가 생겼습니다. 이 문제를 해결하려면 두 개 이상의 코어가 동시에 동일한 메모리 영역을 운영하는 것을 방지해야 합니다. 그렇다면 그것을 피하는 방법은 무엇입니까? 이 기사의 주인공인 잠금 접두어를 소개합니다. 이 명령어에 대한 자세한 설명은 Intel 개발자 매뉴얼 Volume 2 Instruction Set Reference, Chapter 3 Instruction Set Reference A-L을 참조하세요. 여기에는 다음과 같이 해당 섹션이 인용되어 있습니다.

LOCK—Assert LOCK# 신호 접두어

프로세서의 LOCK# 신호가 동반 명령어 실행 중에 어설션되도록 합니다(
명령어를 원자적 명령어로 전환
). 다중 프로세서 환경에서 LOCK# 신호는 신호가 어설션되는 동안 프로세서가 모든 공유 메모리를 독점적으로 사용하도록 보장합니다.위에 설명된 핵심 사항은 다중 프로세서 환경에서 LOCK# 신호가 굵게 강조 표시되었습니다. 처리를 보장할 수 있습니다. 서버는 일부 공유 메모리를 독점적으로 사용합니다. 잠금은

ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD 및 XCHG 명령 앞에 추가할 수 있습니다.

inc 명령어 앞에 잠금 접두어를 추가하면 명령어를 원자적으로 만들 수 있습니다. 여러 코어가 동시에 동일한 inc 명령어를 실행하는 경우 직렬 방식으로 실행되므로 위에서 언급한 상황을 피할 수 있습니다. 그렇다면 여기에 또 다른 질문이 있습니다. 잠금 접두사는 코어가 특정 메모리 영역을 독점적으로 차지하도록 어떻게 보장합니까? 대답은 다음과 같습니다.

인텔 프로세서에는 프로세서의 특정 코어가 특정 메모리 영역을 독점적으로 점유하도록 하는 두 가지 방법이 있습니다. 첫 번째 방법은 버스를 잠그고 특정 코어가 버스를 독점적으로 사용하도록 하는 것이지만 이는 비용이 너무 많이 듭니다. 버스가 잠긴 후에는 다른 코어가 메모리에 액세스할 수 없으므로 다른 코어가 잠시 동안 작동을 멈출 수 있습니다. 두 번째 방법은 일부 메모리 데이터가 프로세서 캐시에 캐시된 경우 캐시를 잠그는 것입니다. 프로세서가 발행한 LOCK# 신호는 버스를 잠그는 것이 아니라 캐시 라인에 해당하는 메모리 영역을 잠급니다. 이 메모리 영역이 잠겨 있는 동안 다른 프로세서는 이 메모리 영역에서 관련 작업을 수행할 수 없습니다. 버스를 잠그는 것과 비교하면 캐시를 잠그는 데 드는 비용은 확실히 더 적습니다. 버스 잠금 및 캐시 잠금에 대한 자세한 설명은 Intel 개발자 매뉴얼 3권 소프트웨어 개발자 매뉴얼, 8장 다중 프로세서 관리를 참조하십시오.

3. 소스 코드 분석

위의 배경 지식을 바탕으로 이제 CAS의 소스 코드를 여유롭게 읽을 수 있습니다. 이번 장의 내용은 java.util.concurrent.atomic 패키지의 Atomic 클래스 AtomicInteger에 있는 CompareAndSet 메소드를 분석해보겠습니다. 복잡하지 않습니다. 코드의 세부사항에 매달리지 않으면 비교적 이해하기 쉽습니다. 다음으로 Windows 플랫폼에서 Atomic::cmpxchg 함수를 분석하겠습니다. 계속 읽어보세요.

public class AtomicInteger extends Number implements java.io.Serializable {

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // 计算变量 value 在类对象中的偏移
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    
    public final boolean compareAndSet(int expect, int update) {
        /*
         * compareAndSet 实际上只是一个壳子,主要的逻辑封装在 Unsafe 的 
         * compareAndSwapInt 方法中
         */
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    // ......
}

public final class Unsafe {
    // compareAndSwapInt 是 native 类型的方法,继续往下看
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
    // ......
}

위 코드는 LOCK_IF_MP 사전 컴파일된 식별자와 cmpxchg 함수로 구성됩니다. 좀 더 명확하게 보기 위해 cmpxchg 함수의 LOCK_IF_MP를 실제 내용으로 대체해 보겠습니다.

// unsafe.cpp
/*
 * 这个看起来好像不像一个函数,不过不用担心,不是重点。UNSAFE_ENTRY 和 UNSAFE_END 都是宏,
 * 在预编译期间会被替换成真正的代码。下面的 jboolean、jlong 和 jint 等是一些类型定义(typedef):
 * 
 * jni.h
 *     typedef unsigned char   jboolean;
 *     typedef unsigned short  jchar;
 *     typedef short           jshort;
 *     typedef float           jfloat;
 *     typedef double          jdouble;
 * 
 * jni_md.h
 *     typedef int jint;
 *     #ifdef _LP64 // 64-bit
 *     typedef long jlong;
 *     #else
 *     typedef long long jlong;
 *     #endif
 *     typedef signed char jbyte;
 */
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value,
                         volatile unsigned int* dest, unsigned int compare_value) {
  assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  /*
   * 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载
   * 函数。相关的预编译逻辑如下:
   * 
   * atomic.inline.hpp:
   *    #include "runtime/atomic.hpp"
   *    
   *    // Linux
   *    #ifdef TARGET_OS_ARCH_linux_x86
   *    # include "atomic_linux_x86.inline.hpp"
   *    #endif
   *   
   *    // 省略部分代码
   *    
   *    // Windows
   *    #ifdef TARGET_OS_ARCH_windows_x86
   *    # include "atomic_windows_x86.inline.hpp"
   *    #endif
   *    
   *    // BSD
   *    #ifdef TARGET_OS_ARCH_bsd_x86
   *    # include "atomic_bsd_x86.inline.hpp"
   *    #endif
   * 
   * 接下来分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函数实现
   */
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
                                       (jint)compare_value);
}

CAS 구현 프로세스는 여기서 완료됩니다. CAS 구현은 프로세서 지원과 불가분의 관계입니다. 위에 코드가 너무 많지만 핵심 코드는 실제로

라는 잠금 접두어가 있는 cmpxchg 명령어입니다.

lock cmpxchg dword ptr [edx], ecx

4.ABA 이슈

CAS를 이야기하면 기본적으로 CAS의 ABA 이슈를 이야기해야 합니다. CAS는 "읽기->비교->쓰기"의 세 단계로 구성됩니다. 스레드 1과 스레드 2가 동시에 CAS 논리를 실행하는 상황을 생각해 보십시오. 두 스레드의 실행 순서는 다음과 같습니다.

시간 1: 스레드 1이 읽기 작업을 수행하고 원래 값 A를 얻은 다음 스레드가 전환되었습니다.
  1. 시간 2: 스레드 2가 CAS 작업을 완료하고 원래 값을 A에서 B
  2. 로 변경합니다. 시간 3: 스레드 2가 CAS 작업을 다시 수행하고 원래 값을 B에서 A
  3. 시간 4로 변경합니다. Thread 1은 실행을 재개하고 값(compareValue)을 원래 값(oldValue)과 비교하여 두 값이 동일한 것으로 확인됩니다. 그런 다음 새 값(newValue)을 메모리에 쓰면 CAS 연산이 완료됩니다.
  4. 위 과정에서 보듯이 스레드 1은 원래 값이 수정된 것을 모르고 자신의 관점에도 변화가 없으므로 계속해서 프로세스를 실행합니다. ABA 문제의 경우 일반적인 해결 방법은 각 CAS 작업에 버전 번호를 설정하는 것입니다. java.util.concurrent.atomic 패키지는 ABA 문제를 처리할 수 있는 원자 클래스 AtomicStampedReference를 제공합니다. 여기서는 특정 구현을 분석하지 않습니다. 관심 있는 친구는 직접 확인할 수 있습니다.

5. 요약

이 글을 쓰며 드디어 글이 끝나갑니다. CAS의 원리 자체는 구현을 포함해 어렵지 않지만 실제로 작성하기는 쉽지 않습니다. 여기에는 약간의 낮은 수준의 지식이 포함되어 있습니다. 이해할 수는 있지만 여전히 이해하기는 어렵습니다. 저의 기초 지식 부족으로 인해 위의 분석 중 일부는 필연적으로 틀릴 수 있습니다. 따라서 오류가 있으면 자유롭게 댓글을 달아주세요. 물론 왜 오류인지 설명하는 것이 가장 좋습니다.

알겠습니다. 이번 기사는 여기까지입니다. 읽어주셔서 감사합니다. 안녕히 계세요.

Appendix

이전 소스코드 분석 섹션에서 사용된 여러 파일 중, 경로는 여기에 게시되어 있습니다. 다음과 같이 모든 사람이 색인을 생성하는 데 도움이 됩니다.

파일 이름Unsafe.javaunsafe.cppatomic.cppatomic_windows_x86 .inline.hpp
Path
openjdk/jdk/src/share/classes/sun/misc/Unsafe.java
openjdk/hotspot/src/share/vm/prims/unsafe.cpp
openjdk/hotspot/src/share/vm/runtime/atomic.cpp
openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp

위 내용은 Java CAS 원리 분석 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제