1. 동기화 문제를 제안하십시오
듀얼 코어 프로세서를 사용하여 두 개의 스레드 A와 B를 실행하고 Core 1은 스레드 A를 실행하고 Core 2는 스레드 B를 실행합니다. 두 스레드는 이제 OBJ라는 객체의 멤버 변수 I에 1을 추가해야합니다. i의 초기 값이 0이라고 가정하면, 이론적으로 두 스레드가 실행 된 후에 I의 값이 2가되어야하지만 실제로 결과는 1 일 가능성이 높습니다.
지금 이유를 분석해 봅시다. 분석의 단순성을 위해 캐시 상황을 고려하지 않습니다. 실제로, 결과가 1 일 가능성을 높이는 캐시가 있습니다. 스레드 a 스레드는 메모리의 변수 i를 커널 1 산술 작동 장치에 읽은 다음 추가 작업을 수행 한 다음 계산 결과를 메모리에 다시 씁니다. 위의 조작은 원자 작동이 아니기 때문에 스레드 B가 메모리에서 I의 값을 읽는 한, 스레드 A가 메모리에 1을 다시 추가하여 I의 값을 쓸 때 (i의 값은이 시간에 0입니다), I의 결과는 확실히 1 인 것으로 보일 것입니다. 스레드 A와 B에 의해 읽은 값은 1에 1을 추가 한 후 1의 값은 1에 1에 1을 씁니다.
가장 일반적인 솔루션은 동기화 키워드를 사용하여 두 스레드에서 i- 가시 가능한 코드에 1을 추가하는 코드와 함께 OBJ 객체를 잠그는 것입니다. 오늘 우리는 원자 패키지에 관련 클래스를 사용하여이를 해결하는 새로운 솔루션을 소개합니다.
2. 자극성 하드웨어 지원
단일 프로세서 시스템 (Uniprocessor)에서 단일 명령어로 완료 될 수있는 작업은 "원자 작업"을 고려할 수 있습니다 (스레드 예약은 인터럽트를 통해 스레드 예약을 완료해야하기 때문에 인터럽트가 발생할 수 있기 때문입니다. 또한 일부 CPU 명령 시스템이 Test_and_Set, Test_and_Clear 및 중요한 자원 상호 배제에 대한 기타 지침을 소개하는 이유입니다. 여러 프로세서가 시스템에서 독립적으로 실행되기 때문에 대칭 다중 프로세서 구조에서 다르며, 단일 명령어로 완료 될 수있는 작업조차 방해가 될 수 있기 때문입니다.
X86 플랫폼에서 CPU는 교육 실행 중에 버스를 잠그는 수단을 제공합니다. CPU 칩에는 리드 #hlockpin이 있습니다. 접두사 "잠금"이 어셈블리 언어 프로그램의 명령에 추가되면, 어셈블리 머신 코드는 CPU 가이 명령어를 실행할 때 #hlockpin의 잠재력을 낮추고이 명령이 끝날 때까지 해제하여 버스를 잠그게합니다. 이런 식으로, 같은 버스의 다른 CPU는 버스를 통해 메모리에 액세스 할 수 없으므로 다중 프로세서 환경 에서이 명령의 원자력을 보장합니다. 물론 모든 지침이 잠금으로 접두사를 할 수있는 것은 아닙니다. ADD, ADC, BTC, BTR, BTS, CMPXCHG, DEC, Inc, NEG, NOT, OR, SBB, SUB, XOR, XADD 및 XCHG 명령어는 "잠금"지침으로 접두사를 만들어 원자 운영을 실현할 수 있습니다.
Atomic의 핵심 작동은 CAS (CompxCHG 명령어를 사용하여 구현 된 CASIANDSET,이 원자 명령어)입니다. 이 명령어에는 변수의 메모리 값 V (값의 약어), 변수의 현재 예상 값 E (예외의 약어), 변수의 값이 업데이트를 원하는 (업데이트의 약어) 3 개의 피연산자가 있습니다. 메모리 값이 현재 예상 값과 동일하면 변수의 업데이트 된 값은 변수에 의해 덮어 씁니다. 의사 코드는 다음과 같이 실행됩니다.
if (v == e) {v = u return true} else {return false}이제 CAS 작업을 사용하여 위의 문제를 해결합니다. 스레드 B는 메모리의 변수 I을 임시 변수로 읽은 다음 (이 시간에 읽는 값이 0이라고 가정), I의 값을 Core1의 산술 작동 단위로 읽습니다. 다음으로 1을 추가하여 임시 변수의 값이 i의 현재 값과 동일한 지 비교합니다. 메모리에서 i의 값이 작동 장치 (즉, i+1)의 결과 값과 동일하다면 (이 부분은 CAS 작동이므로, 원자력 작동이며, 방해 할 수없고 다른 스레드에서 CAS 조작은 동시에 실행할 수 없음), 그렇지 않으면 명령 실행이 실패합니다. 명령어가 실패하면 스레드 A가 I의 값을 1 씩 증가 시켰음을 의미합니다. 이로부터 두 스레드로 읽은 값이 처음에 0이면 CAS 작동을 동시에 실행할 수 없기 때문에 하나의 스레드의 CAS 작업 만 성공할 수 있습니다. CAS 작업을 실행하지 못하는 스레드의 경우 CAS 작업이 반복적으로 실행되는 한 확실히 성공할 것입니다. 스레드 차단이 없다는 것을 알 수 있으며, 이는 동기화 원리와 본질적으로 다릅니다.
3. 원자 패키지 및 소스 코드 분석 소개
원자 패키지에서 클래스의 기본 기능은 다중 스레드 환경에서 여러 스레드가 단일 (기본 유형 및 참조 유형 포함) 변수에서 동시에 작동 할 때, 배타적 인 경우, 여러 스레드가 변수의 값을 동시에 업데이트하면 하나의 스레드 만 성공할 수 있고 실행이 계속해서 스핀 잠금을 시도하고 성공할 때까지 계속해서 성공할 수 있다는 것입니다.
Atomic Series 클래스의 핵심 방법은 안전하지 않은 클래스에서 몇 가지 로컬 방법을 호출합니다. 우리는 먼저 한 가지는 안전하지 않은 클래스라는 것을 알고 있습니다. 이 클래스에는 많은 직접 메모리 할당 및 원자 운영 호출을 포함하여 C 코드에 대한 많은 작업이 포함되어 있습니다. 비 안전으로 표시되는 이유는이 영역에서 많은 수의 방법 호출이 보안 위험이 있으며 신중하게 사용해야한다고 말하면 심각한 결과를 초래할 것입니다. 예를 들어, 안전하지 않은 것을 통해 메모리를 할당 할 때 특정 영역을 직접 지정하면 C ++와 같은 일부 포인터가 다른 프로세스로 경계를 넘을 수 있습니다.
원자 패키지의 클래스는 운영 데이터 유형에 따라 4 개의 그룹으로 나눌 수 있습니다.
AtomicBoolean,AtomicInteger,AtomicLong
스레드 안전을위한 기본 유형의 원자 연산
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
전체 배열이 아니라 배열의 단일 요소에서 작동하는 배열 유형의 스레드 안전 원자 작동
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
반사 원칙 객체의 기본 유형 (긴 정수, 정수 및 참조 유형)을 기반으로 한 스레드 안전 작업
AtomicReference,AtomicMarkableReference,AtomicStampedReference
ABA 문제를 예방하는 참조 유형의 스레드 안전 참조 유형 및 원자력
우리는 일반적으로 Atomicinteger, Atomicreference 및 AtomicStampedReference를 사용합니다. 이제 원자 패키지에서 원자 정수의 소스 코드를 분석하겠습니다. 다른 클래스의 소스 코드는 원칙적으로 비슷합니다.
1. 매개 변수 생성기
public atomicinteger (int initialvalue) {value = initialvalue;}생성자 함수에서 볼 수 있듯이 값은 멤버 변수 값에 저장됩니다.
개인 휘발성 INT 값;
멤버 변수 값은 휘발성 유형으로 선언되며, 이는 여러 스레드의 가시성을 보여줍니다. 즉, 모든 스레드의 수정은 다른 스레드에서 즉시 표시됩니다.
2. CompareAndset 방법 (값의 값은 내부 및 ValueOffset을 통해 전달됩니다)
공개 Final Boolean CompareAndset (int reposs, int update) {return unsafe.compareAndswapint (this, valueOffset, expling, update);}이 방법은 가장 핵심 CAS 작업입니다
3. getAndandset 메소드, 비교 세트 방법이 호출됩니다
public final int getAndset (int newValue) {for (;;) {int current = get (); if (compareAndset (current, newValue)) current를 반환합니다. }}IF (CompareAndset (current, newValue))을 실행하기 전에 다른 스레드가 값의 값을 변경하면 값의 값은 현재 값과 달라야합니다. CompareAndset이 실행되지 않으면 값의 값만 다시 구입 한 다음 성공할 때까지 계속 비교할 수 있습니다.
4. I ++ 구현
public final int getAndincrement () {for (;;) {int current = get (); int next = current + 1; if (compareAndset (current, next))을 반환합니다. }}5. ++의 구현 i
public final int excrementandget () {for (;;) {int current = get (); int next = current + 1; if (compareAndset (current, next)) 다음에 반환; }}4. Atomicinteger 예제를 사용하십시오
다음 프로그램은 Atomicinteger를 사용하여 티켓 판매 프로그램을 시뮬레이션합니다. 두 프로그램은 실행 결과에서 동일한 티켓을 판매하지 않으며 티켓을 음수로 판매하지도 않습니다.
패키지 javaleAnning; import java.util.concurrent.atomic.atomicinteger; public class selltickets {atomicinteger tickets = new atomicinteger (100); 클래스 판매자는 runnable {@override public void run () {while (tictets.get ()> 0) {int tmp = tickets.get (); if (tickets.compareAndset (tmp, tmp-1)) {system.out.println (thread.currentthread (). getName ()+""+tmp);}}}} public static void main (string [] args) {Selltickets st = new Selltickets (); New Sell (Sellew Seller), "sellera"). start (); new Thread (St.New seller (), "sellerb"). start ();}}5. ABA 문제
위의 예는 결과를 완전히 정확하게 실행합니다. 이는 두 (또는 그 이상의) 스레드가 동일한 방향으로 데이터에서 작동한다는 사실을 기반으로합니다. 위의 예에서는 두 스레드가 티켓에서 줄어 듭니다. 예를 들어, 여러 스레드가 공유 대기열에서 객체 등록 작업을 수행하면 Atomicreference 클래스를 통해 올바른 결과를 얻을 수 있습니다 (실제로 AQS로 유지되는 대기열의 경우). 그러나 여러 스레드가 등록하거나 상장 될 수 있습니다. 즉, 데이터의 작동 방향은 일치하지 않으므로 ABA가 발생할 수 있습니다.
이제 ABA 문제를 설명하기 위해 상대적으로 이해하기 쉬운 예를 들어 봅시다. 두 개의 스레드 T1과 T2가 있다고 가정 하고이 두 스레드는 동일한 스택에서 스태킹 및 스태킹 작업을 수행한다고 가정합니다.
우리는 Atomicreference에 의해 정의 된 꼬리를 사용하여 스택의 상단 위치를 저장합니다.
Atomicreference <t> 꼬리;
T1 스레드가 롤아웃 될 준비가되었다고 가정하면 스태킹 작업을 위해 스택의 상단 위치를 그림 1과 같이 SP에서 Newsp로 CAS 작업까지 스택의 상단 위치를 업데이트하면됩니다. 그러나 T1 스레드가 Tail.comPareAndset (SP, Newsp)을 실행하기 전에 시스템은 스레드 스케줄링을 수행하고 T2 스레드는 실행을 시작합니다. T2는 세 가지 작업을 수행합니다. A는 스택을 벗어 났고 B는 스택에서 벗어난 다음 A가 스택에 있습니다. 이 시점에서 시스템은 다시 일정을 잡기 시작하고 T1 스레드는 스태킹 작업을 계속 수행하지만, T1 스레드의 관점에서도 스택 상단의 요소는 여전히 A입니다 (즉, T1은 여전히 B가 여전히 스택 A의 상단에있는 다음 요소라고 생각합니다. 스택의 포인터는 노드 B를 가리 킵니다. 실제로, B는 더 이상 스택에 존재하지 않습니다. T1이 스택을 벗어난 후 결과는 그림 3에 나와 있으며, 이는 분명히 올바른 결과가 아닙니다.
6. ABA 문제에 대한 해결책
AtomicmarkableReference, AtomicStampedReference를 사용하십시오. 위에서 언급 한 두 원자 클래스를 사용하여 작업을 수행하십시오. CompareAndset 명령어를 구현할 때는 객체의 이전 값과 예상 값을 비교해야 할뿐만 아니라 현재 (작동) 스탬프 값과 예상 (작동) 스탬프 값을 비교해야합니다. 모두 동일하게 참을 때만 CompareAndset 메소드가 성공할 수 있습니다. 업데이트가 성공할 때마다 스탬프 값이 변경되며 스탬프 값의 설정은 프로그래머 자신에 의해 제어됩니다.
Public Boolean CompareAndset (V expectoreference, v newReference, int expossStamp, int newStamp) {pair <v> current = pair; return expliceperence == current.reference && explicStamp == current.stamp && ((newReference == current.reference && newstamp == current.stamp) || caspair (newRefore.of));이 시점에서 CompareAndset 메소드에는 Expection, NewReference, ExpectStamp, NewStamp의 네 가지 매개 변수가 필요합니다. 이 방법을 사용할 때는 예상 스탬프 값이 업데이트 스탬프 값과 같지 않도록해야합니다. 일반적으로 newstamp = expectStamp+1입니다
위의 예를 들어보십시오
스레드 T1이 스택 앞에 있다고 가정합니다.
스레드 T2 실행 : A가 릴리스 된 후 SP가 B를 가리키고 스탬프 값은 101이됩니다.
B가 릴리스 된 후 SP는 C를 가리키고 스탬프 값은 102가됩니다.
A가 스택에 넣은 후 SP는 A를 가리키고 스탬프 값은 103이됩니다.
Thread T1은 CompareAndset 문을 계속 실행하고 SP가 여전히 A를 가리키지 만 스탬프 값 100의 예상 값은 현재 값 103과 다르다는 것을 알게됩니다. 따라서 비교 세트는 실패합니다. 신문의 가치 (현재, 신문은 C를 가리킬 것입니다)와 스탬프 값 103의 예상 값을 다시 수행 한 다음 다시 비교 세트 작업을 수행해야합니다. 이런 식으로 A는 스택을 성공적으로 롤아웃합니다. SP는 C를 가리 킵니다.
CompareAndset은 한 번에 하나의 값 만 변경할 수 있고 동시에 NewReference 및 NewStamp를 변경할 수 없으므로 구현하는 동안 쌍 클래스가 내부적으로 정의되어 NewReference와 NewStamp를 하나의 객체로 전환합니다. CAS 작업을 수행 할 때는 실제로 쌍 객체에서 작업입니다.
개인 정적 클래스 쌍 <t> {최종 T 참조; 최종 int 스탬프; 개인 쌍 (t 참조, int Stamp) {this.reference = 참조; this.stamp = 스탬프; } static <t> pair <t> of (t reference, int stamp) {return new Pair <t> (참조, 스탬프); }}AtomicmarkableReference의 경우 스탬프 값은 부울 변수이며 AtomicStampedReference의 스탬프 값은 정수 변수입니다.
요약
위의 것은 Java의 원자 패키지의 구현 원리 및 응용 프로그램에 대한이 기사의 간단한 토론에 관한 것입니다. 모든 사람에게 도움이되기를 바랍니다. 관심있는 친구는이 사이트의 다른 관련 주제를 계속 참조 할 수 있습니다. 단점이 있으면 메시지를 남겨 두십시오.