원자적 연산의 등장을 위한 전제 조건
원자성 작업이 작동하는 방식을 이해하는 데 도움이 되는 이 예를 살펴보겠습니다.
public class Counter {
int count;
public void increment() {
count++;
}
}
하나의 스레드가 있으면 모든 것이 잘 작동하지만 멀티스레딩을 추가하면 잘못된 결과를 얻게 되며 증가 작업이 하나의 작업이 아니라 3개이기 때문입니다. 현재 값을 가져오기 위한 요청세다, 그런 다음 1씩 증가시키고 다시 씁니다.세다.
그리고 두 스레드가 변수를 증가시키려고 하면 데이터가 손실될 가능성이 큽니다. 즉, 두 스레드 모두 100을 수신하므로 결과적으로 둘 다 예상 값 102 대신 101을 씁니다.
그리고 그것을 해결하는 방법? 자물쇠를 사용해야 합니다. synchronized 키워드는 이 문제를 해결하는 데 도움이 되며, 이를 사용하면 한 번에 하나의 스레드가 메서드에 액세스할 것이라는 보장이 제공됩니다.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
또한 스레드 간 참조의 올바른 가시성을 보장하는 volatile 키워드를 추가해야 합니다. 위에서 그의 작업을 검토했습니다.
그러나 여전히 단점이 있습니다. 가장 큰 것은 성능입니다. 많은 스레드가 잠금을 획득하려고 시도하고 하나가 쓰기 기회를 얻는 시점에서 나머지 스레드는 스레드가 해제될 때까지 차단되거나 일시 중단됩니다.
이러한 모든 프로세스, 차단, 다른 상태로의 전환은 시스템 성능에 매우 비쌉니다.
원자성 작업
이 알고리즘은 비교 및 교환(CAS, 비교 및 교환, 데이터 무결성을 보장하고 이에 대한 많은 연구가 이미 있음)과 같은 저수준 기계 명령어를 사용합니다.
일반적인 CAS 작업은 세 개의 피연산자에서 작동합니다.
- 작업을 위한 메모리 공간(M)
- 변수의 기존 기대값(A)
- 설정할 새 값(B)
CAS는 M을 B로 원자적으로 업데이트하지만 M의 값이 A와 동일한 경우에만 그렇지 않으면 아무런 조치도 취하지 않습니다.
첫 번째와 두 번째 경우에는 M 값이 반환되므로 값 가져오기, 값 비교 및 업데이트의 세 단계를 결합할 수 있습니다. 그리고 이 모든 것이 기계 수준에서 하나의 작업으로 바뀝니다.
다중 스레드 응용 프로그램이 변수에 액세스하고 업데이트를 시도하고 CAS가 적용되는 순간 스레드 중 하나가 변수를 가져오고 업데이트할 수 있습니다. 그러나 잠금과 달리 다른 스레드는 값을 업데이트할 수 없다는 오류를 받게 됩니다. 그런 다음 추가 작업으로 이동하며 이러한 유형의 작업에서는 전환이 완전히 제외됩니다.
이 경우 CAS 작업이 성공적으로 수행되지 않은 상황을 처리해야 하므로 논리가 더 어려워집니다. 작업이 성공할 때까지 코드가 이동하지 않도록 코드를 모델링할 뿐입니다.
원자 유형 소개
int 유형의 가장 간단한 변수에 대해 동기화를 설정해야 하는 상황에 처한 적이 있습니까 ?
우리가 이미 다룬 첫 번째 방법은 volatile + synchronized 를 사용하는 것입니다 . 그러나 특별한 Atomic* 클래스도 있습니다.
CAS를 사용하면 첫 번째 방법에 비해 작업이 더 빠르게 수행됩니다. 또한 값을 추가하고 작업을 증가 및 감소시키는 특별하고 매우 편리한 방법이 있습니다.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray 는 작업이 원자적인 클래스입니다. 아래에서 우리는 그들과의 작업을 분석할 것입니다.
AtomicInteger
AtomicInteger 클래스는 확장된 원자 연산을 제공하는 것 외에도 원자적으로 읽고 쓸 수 있는 int 값 에 대한 연산을 제공합니다 .
변수를 읽고 쓰는 것처럼 작동하는 get 및 set 메서드가 있습니다 .
즉, 이전에 이야기한 동일한 변수의 후속 수신과 함께 "이전에 발생"합니다. 원자성 compareAndSet 메서드에는 이러한 메모리 일관성 기능도 있습니다.
새 값을 반환하는 모든 작업은 원자적으로 수행됩니다.
int addAndGet (int 델타) | 현재 값에 특정 값을 추가합니다. |
부울 compareAndSet(예상 int, 업데이트 int) | 현재 값이 예상 값과 일치하면 주어진 업데이트 값으로 값을 설정합니다. |
int 감소 및 Get() | 현재 값을 1씩 감소시킵니다. |
int getAndAdd(int 델타) | 주어진 값을 현재 값에 더합니다. |
int getAndDecrement() | 현재 값을 1씩 감소시킵니다. |
정수 getAndIncrement() | 현재 값을 1씩 증가시킵니다. |
int getAndSet(int newValue) | 주어진 값을 설정하고 이전 값을 반환합니다. |
int 증가 및 Get() | 현재 값을 1씩 증가시킵니다. |
게으른 세트(int newValue) | 마지막으로 주어진 값으로 설정합니다. |
부울 weakCompareAndSet(예상, 업데이트 정수) | 현재 값이 예상 값과 일치하면 주어진 업데이트 값으로 값을 설정합니다. |
예:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION