Java 하이 병렬 4: 자물쇠 없음 상세 정보
1 무쇄류의 원리 상세
1.1 CAS
CAS 알고리즘의 프로세스는 다음과 같습니다. 이 알고리즘은 3개의 매개 변수 CAS(V, E, N)를 포함합니다.V는 업데이트할 변수를 나타내고 E는 예상치를 나타내며 N은 새 값을 나타냅니다.V로만
값이 E 값과 같아야 V의 값이 N으로 설정됩니다. V 값과 E 값이 다르면 다른 스레드가 업데이트되었음을 나타냅니다. 현재 스레드가 무엇입니까?
안 해.마지막으로 CAS는 현재 V의 실제 값을 반환합니다.CAS 작업은 항상 성공적이라고 생각하는 낙관적인 태도로 진행됩니다.
조작하다.여러 스레드가 하나의 변수를 CAS로 동시에 조작할 때 하나만 이기고 업데이트에 성공하면 나머지는 모두 실패합니다.실패한 스레드
끊기지 않습니다. 실패를 알리고 다시 시도할 수 있습니다. 물론 실패한 라인이 작업을 포기하는 것도 허용합니다.이러한 원리를 바탕으로 CAS
조작이 즉시 잠기지 않고 다른 라인이 현재 라인에 대한 방해를 발견하고 적절한 처리를 할 수 있다.
CAS의 절차가 너무 많아서 V와 E가 같다고 판단한 후에 값을 부여하려고 할 때 라인을 전환하고 값을 변경할 수 있는지 알 수 있습니다.데이터가 일치하지 않는데요?
사실 이 걱정은 쓸데없는 것이다.CAS의 전체 작업 과정은 하나의 CPU 명령으로 이루어진 원자 작업입니다.
1.2 CPU 명령
CAS의 CPU 명령은 cmpxchg입니다.
명령 코드는 다음과 같습니다.
/*
accumulator = AL, AX, or EAX, depending on whether
a byte, word, or doubleword comparison is being performed
*/
if(accumulator == Destination) {
ZF = 1;
Destination = Source;
}
else {
ZF = 0;
accumulator = Destination;
}
목표 값과 레지스터의 값이 같으면 점프 로고를 설치하고 원시 데이터를 목표 안에 설정합니다.만약 기다리지 않는다면, 점프 표지를 설치하지 않을 것이다.자바에는 자물쇠 없는 클래스가 많이 제공되고 있습니다. 다음은 자물쇠 없는 클래스를 소개합니다.
2 무분별한 사용
우리는 이미 자물쇠가 막히는 것보다 효율이 훨씬 높다는 것을 안다.자바가 어떻게 이런 자물쇠 없는 종류를 실현하는지 봅시다.
2.1. AtomicInteger
AtomicInteger는 Integer와 마찬가지로 Number 클래스를 상속합니다.
public class AtomicInteger extends Number implements java.io.Serializable
AtomicInteger에는 다음과 같은 CAS 작업이 있습니다.
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
여기서 unsafe를 설명해 드리겠습니다.compare And Swap Int 방법은this 클래스의 편이량이value Offset인 변수 값이 기대치expect와 같으면 이 변수의 값을 업데이트로 설정하는 것입니다.
사실 오프셋이 value Offset인 변수가 value예요.
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
우리가 이전에 말했듯이 CAS는 실패할 수 있지만 실패의 대가는 매우 작기 때문에 일반적인 실현은 모두 무한 순환 체내에서 성공할 때까지 이루어진다.
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
2.2 Unsafe클래스 이름에서 알 수 있듯이 Unsafe 작업은 비안전한 작업입니다. 예를 들어
오프셋 값을 기준으로 설정합니다. (방금 설명한 Atomic Integer에서 이 기능을 보았습니다.)
park () (이 라인을 멈추면 나중에 블로그에서 언급할 것)
기본 CAS 작업
비공개 API, JDK 버전에 따라 차이가 클 수 있음
2.3. AtomicReference
앞에서 이미 Atomic Integer를 언급했는데 물론 Atomic Boolean, Atomic Long 등도 대동소이하다.
여기서 소개할 것은 Atomic Reference입니다.
AtomicReference는 템플릿 클래스입니다.
public class AtomicReference
그것은 임의의 유형의 데이터를 봉인할 수 있다.
예를 들어 String
package test;
import java.util.concurrent.atomic.AtomicReference;
public class Test
{
public final static AtomicReference<String> atomicString = new AtomicReference<String>("hosee");
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
final int num = i;
new Thread() {
public void run() {
try
{
Thread.sleep(Math.abs((int)Math.random()*100));
}
catch (Exception e)
{
e.printStackTrace();
}
if (atomicString.compareAndSet("hosee", "ztk"))
{
System.out.println(Thread.currentThread().getId() + "Change value");
}else {
System.out.println(Thread.currentThread().getId() + "Failed");
}
};
}.start();
}
}
}
결과:10Failed
13Failed
9Change value
11Failed
12Failed
15Failed
17Failed
14Failed
16Failed
18Failed
값을 수정할 수 있는 라인이 하나밖에 없고 뒤의 라인은 더 이상 수정할 수 없습니다.
2.4.AtomicStampedReference
저희가 CAS 조작에 문제가 있다는 걸 알게 될 거예요.
예를 들어 이전의 Atomic Integer의 increment And Get 방법.
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
현재value=1이 어떤 스레드 intcurrent=get () 를 실행하면 다른 스레드로 전환합니다. 이 스레드는 1을 2로 바꾸고 또 다른 스레드는 2를 1로 바꿉니다.이때 다시 처음에 그 라인으로 전환합니다.value는 여전히 1이기 때문에 CAS 조작을 실행할 수 있습니다. 물론 덧셈은 문제가 없습니다. 만약에 어떤 상황이 데이터의 상태에 민감할 때 이런 과정은 허용되지 않습니다.AtomicStampedReference 클래스가 필요합니다.
그 내부에는 값과 시간 스탬프를 봉인하기 위한 Pair 클래스가 있습니다.
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
이런 종류의 주요 사상은 시간 스탬프를 넣어 매번 변화를 표시하는 것이다.//비교 설정 매개 변수 순서: 기대치 쓰기 새 시간 스탬프 새 시간 스탬프
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
당기 희망 값은 현재 값이고 기대 시간 스탬프가 현재 시간 스탬프와 같을 때 새 값을 쓰고 새 시간 스탬프를 업데이트합니다.여기에 Atomic Stamped Reference를 사용한 장면을 들어보면 잘 어울리지 않을 것 같지만 좋은 장면은 생각지도 못했다.
장면 배경은 모 회사가 잔액이 적은 사용자에게 무료로 충전을 해주지만 사용자마다 한 번만 충전할 수 있다는 것이다.
package test;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test
{
static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(
19, 0);
public static void main(String[] args)
{
for (int i = 0; i < 3; i++)
{
final int timestamp = money.getStamp();
new Thread()
{
public void run()
{
while (true)
{
while (true)
{
Integer m = money.getReference();
if (m < 20)
{
if (money.compareAndSet(m, m + 20, timestamp,
timestamp + 1))
{
System.out.println(" , :"
+ money.getReference());
break;
}
}
else
{
break;
}
}
}
};
}.start();
}
new Thread()
{
public void run()
{
for (int i = 0; i < 100; i++)
{
while (true)
{
int timestamp = money.getStamp();
Integer m = money.getReference();
if (m > 10)
{
if (money.compareAndSet(m, m - 10, timestamp,
timestamp + 1))
{
System.out.println(" 10 , :"
+ money.getReference());
break;
}
}else {
break;
}
}
try
{
Thread.sleep(100);
}
catch (Exception e)
{
// TODO: handle exception
}
}
};
}.start();
}
}
코드를 설명하자면 세 개의 라인이 사용자에게 충전을 하고 있으며, 사용자의 잔액이 20위안보다 적을 때 사용자에게 20위안을 충전한다.100개의 라인이 소비되고 있는데, 매번 10위안을 소비한다.사용자는 처음에 9위안이 있는데, Atomic Stamped Reference를 사용하여 실현할 때, 매번 조작할 때마다 스탬프 +1을 충전할 수 있기 때문이다.실행 결과:충전 성공, 잔액: 39
소비 10원, 잔액: 29
소비 10원, 잔액: 19
소비 10원, 잔액: 9
Atomic Reference
충전 성공, 잔액: 39
소비 10원, 잔액: 29
소비 10원, 잔액: 19
충전 성공, 잔액: 39
소비 10원, 잔액: 29
소비 10원, 잔액: 19
충전 성공, 잔액: 39
소비 10원, 잔액: 29
2.5. AtomicIntegerArray
Atomic Integer와 비교했을 때, 수조의 실현은 단지 하표가 하나 더 많은 것에 불과하다.
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
그것의 내부는 단지 일반적인array를 봉인했을 뿐이다
private final int[] array;
재미있는 것은 2진수의 전도 0을 이용하여 수조의 편이량을 계산한 것이다.
shift = 31 - Integer.numberOfLeadingZeros(scale);
전도 0은 예를 들어 8위가 1200001100을 나타내면 전도 0은 1 앞의 0의 개수로 4이다.
구체적인 편이량을 어떻게 계산하는지 여기에서는 더 이상 소개하지 않겠다.
2.6. AtomicIntegerFieldUpdater
Atomic Integer Field Updater 클래스의 주요 역할은 일반 변수도 원자 조작을 즐길 수 있도록 하는 것이다.
예를 들어 원래 하나의 변수가 int형이고 많은 곳에서 이 변수를 응용했지만 어떤 장면에서 int형을 Atomic Integer로 바꾸고 싶지만 직접 유형을 바꾸면 다른 지역의 응용을 고쳐야 한다.Atomic Integer Field Updater는 이러한 문제를 해결하기 위해 만들어진 것입니다.
package test;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class Test
{
public static class V{
int id;
volatile int score;
public int getScore()
{
return score;
}
public void setScore(int score)
{
this.score = score;
}
}
public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");
public static AtomicInteger allscore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException
{
final V stu = new V();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++)
{
t[i] = new Thread() {
@Override
public void run()
{
if(Math.random()>0.4)
{
vv.incrementAndGet(stu);
allscore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++)
{
t[i].join();
}
System.out.println("score="+stu.getScore());
System.out.println("allscore="+allscore);
}
}
상기 코드는 score를 사용하여 AtomicIntegerFieldUpdater를 AtomicInteger로 변경합니다.라인의 안전을 보증했다.여기에서allscore를 사용하여 검증합니다. 만약score와allscore 수치가 같다면 라인이 안전하다는 것을 설명합니다.
설명:
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
JPA + QueryDSL 계층형 댓글, 대댓글 구현(2)이번엔 전편에 이어서 계층형 댓글, 대댓글을 다시 리팩토링해볼 예정이다. 이전 게시글에서는 계층형 댓글, 대댓글을 구현은 되었지만 N+1 문제가 있었다. 이번에는 그 N+1 문제를 해결해 볼 것이다. 위의 로직은 이...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.