Java 하이 병렬 4: 자물쇠 없음 상세 정보

[고병발 자바1] 앞에서 무자물쇠의 개념을 언급했는데 jdk 원본에 대량의 무자물쇠 응용이 있기 때문에 여기서 무자물쇠를 소개합니다.
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  implements java.io.Serializable
그것은 임의의 유형의 데이터를 봉인할 수 있다.
예를 들어 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 또는 Atomic Integer를 사용하면 여러 번 충전할 수 있습니다.
충전 성공, 잔액: 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 수치가 같다면 라인이 안전하다는 것을 설명합니다.
설명:
  • Updater는 가시 범위 내의 변수만 수정할 수 있습니다.Updater가 반사를 사용하여 이 변수를 얻기 때문입니다.변수가 보이지 않으면 오류가 발생합니다.예를 들어 어떤 변수가private라고 밝히면 안 되는데..
  • 변수가 정확하게 읽히는 것을 확보하기 위해서는volatile 형식이어야 합니다.만약 우리가 원래 코드에서 이 유형을 설명하지 않았다면, 간단하게 설명하면 된다. 이것은 무슨 문제를 일으키지 않을 것이다
  • CAS 작업은 대상 실례의 오프셋을 통해 직접 값을 부여하기 때문에 static 필드 (Unsafe.objectFieldOffset () 는 정적 변수를 지원하지 않습니다.
  • 좋은 웹페이지 즐겨찾기