Java에서 AtomicReference를 언제 사용해야합니까?


311

우리는 언제 사용 AtomicReference합니까?

모든 멀티 스레드 프로그램에서 객체를 생성해야합니까?

AtomicReference를 사용해야하는 간단한 예를 제공하십시오.

답변:


215

원자 기준은 모니터 기반 동기화가 적합하지 않은 기준에 대해 간단한 원자 (즉, 스레드 안전 , 사소하지 않은) 작업 을 수행해야하는 설정에서 사용해야합니다 . 마지막으로 확인한대로 객체의 상태가 유지되는 경우에만 특정 필드를 확인하려고한다고 가정합니다.

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

원자 참조 시맨틱 때문에을 cache사용하지 않고 오브젝트가 스레드간에 공유되는 경우에도이를 수행 할 수 있습니다 synchronized. 일반적으로 수행중인 작업을 알지 못하면 동기화 기 또는 java.util.concurrent프레임 워크를 사용하는 것이 좋습니다 Atomic*.

이 주제를 소개하는 두 가지 훌륭한 데드 트리 참조 :

(이것이 항상 참인지는 모르겠습니다) 참조 할당 (즉, =)은 자체가 원자 적입니다 ( 원시적 64 비트 유형을 업데이트 long하거나 double원자 적이 지 않을 수도 있음).하지만 64 비트라도 참조를 업데이트하는 것은 항상 원자 적입니다 )를 명시 적으로 사용하지 않고 Atomic*. Java 언어 사양 3, 섹션 17.7을
참조하십시오 .


43
내가 틀렸다면 바로 잡으십시오. 그러나 이것을 필요로하는 열쇠는 "compareAndSet"을 수행해야하기 때문입니다. 내가해야 할 모든 것이 설정되면 참조 업데이트 자체가 원자이기 때문에 AtomicObject가 전혀 필요하지 않습니까?
sMoZely

cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))를 수행하는 것이 안전합니까? 즉, 계산을 인라인합니까?
kaqqao

4
@veggen Java의 함수 인수는 함수 자체보다 먼저 평가되므로 인라인은이 경우에 차이가 없습니다. 예, 안전합니다.
Dmitry

29
@sMoZely 즉 정확하지만 사용하지 않는 경우 AtomicReference사용자가 변수를 표시해야 volatile그동안 때문에 런타임이 해당 참조 할당을 보장 원자되면, 컴파일러는 변수가 다른 스레드에 의해 수정되지 않는 것을 가정하에 최적화를 수행 할 수있다.
kbolino

1
@BradCupit은 " 사용 하지 않는 경우 AtomicReference"라고 말합니다. 당신 그것을 사용한다면, 내 조언은 반대 방향으로 가고 final컴파일러가 그에 따라 최적화 할 수 있도록 표시하는 것 입니다.
kbolino

91

원자 참조는 여러 스레드간에 불변 개체의 상태를 공유하고 변경해야 할 때 사용하기에 이상적입니다. 그것은 매우 조밀 한 진술이므로 조금 세분화 할 것입니다.

첫째, 불변 개체는 시공 후 효과적으로 변경되지 않는 개체입니다. 불변 객체의 메서드는 종종 같은 클래스의 새 인스턴스를 반환합니다. 예를 들어 Long과 Double의 래퍼 클래스뿐만 아니라 String도 포함됩니다. ( JVM 불변 객체 에서 프로그래밍 동시성에 따르면 현대 동시성의 중요한 부분입니다).

다음으로, AtomicReference가 공유 값을 공유하는 휘발성 개체보다 나은 이유는 무엇입니까? 간단한 코드 예제는 차이점을 보여줍니다.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

현재 값을 기준으로 해당 휘발성 필드에서 참조하는 문자열을 수정할 때마다 먼저 해당 객체에 대한 잠금을 얻어야합니다. 이렇게하면 다른 스레드가 들어오는 동안 새 문자열 연결 중에 값이 변경되는 것을 방지 할 수 있습니다. 그런 다음 스레드가 다시 시작되면 다른 스레드의 작업을 방해합니다. 그러나 솔직히 그 코드가 작동하고 깨끗해 보이며 대부분의 사람들을 행복하게 할 것입니다.

약간의 문제가 있습니다. 느립니다. 특히 해당 잠금 개체에 대해 많은 경합이있는 경우. 대부분의 잠금에는 OS 시스템 호출이 필요하기 때문에 스레드는 다른 프로세스를 처리하기 위해 CPU에서 컨텍스트를 차단하고 차단합니다.

다른 옵션은 AtomicRefrence를 사용하는 것입니다.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

왜 이것이 더 좋은가? 솔직히 그 코드는 이전보다 조금 덜 깨끗합니다. 그러나 AtomicRefrence의 후드에서 발생하는 중요한 사항이 있습니다. 그것은 비교와 스왑입니다. OS 호출이 아닌 단일 CPU 명령으로 전환이 발생합니다. 그것은 CPU에 대한 단일 명령입니다. 또한 잠금 장치가 없기 때문에 잠금 장치가 작동되는 경우 상황 전환이 없으므로 시간이 훨씬 절약됩니다!

catch는 AtomicReferences의 경우 .equals () 호출을 사용하지 않고 대신 예상 값에 대한 == 비교입니다. 따라서 루프에서 가져 오기에서 리턴 된 실제 오브젝트가 예상인지 확인하십시오.


14
두 예제는 다르게 동작합니다. worked동일한 의미를 얻으려면 반복해야합니다 .
CurtainDog

5
AtomicReference 생성자 내에서 값을 초기화해야한다고 생각합니다. 그렇지 않으면 shared.set을 호출하기 전에 다른 스레드에서 여전히 null 값을 볼 수 있습니다. (하지 않는 한 shared.set은 정적 초기화에서 실행됩니다.)
Henno 베르 메울 렌에게

8
두 번째 예에서는 Java 8부터 다음과 같은 것을 사용해야합니다. shared.updateAndGet ((x)-> (x + "lets add something")); ... 작동 할 때까지 .compareAndSet를 반복적으로 호출합니다. 그것은 항상 성공할 동기 블록과 같습니다. 전달한 람다는 여러 번 호출 될 수 있기 때문에 부작용이 없는지 확인해야합니다.
Tom Dibble

2
휘발성 String sharedValue를 만들 필요는 없습니다. 동기화 된 (잠금)은 관계 전에 발생을 설정하기에 충분합니다.
Jai Pandit

2
문자 그대로 불변 객체의 상태를 변경할 수 없기 때문에 "... 불변 객체의 상태 변경"은 정확하지 않습니다. 이 예제는 하나의 불변 객체 인스턴스에서 다른 인스턴스로 참조를 변경하는 방법을 보여줍니다. 나는 그것이 pedantic이라는 것을 알고 있지만 혼란스러운 Thread 논리가 어떻게 될 수 있는지 강조 할 가치가 있다고 생각합니다.
Mark Phillips

30

AtomicReference의 사용 사례는 다음과 같습니다.

숫자 범위로 작동하고 개별 AtmomicInteger 변수를 사용하여 하한 및 상한을 유지하는이 클래스를 고려하십시오.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

setLower 및 setUpper는 모두 확인 후 동작 시퀀스이지만 원자를 만들기에 충분한 잠금을 사용하지 않습니다. 숫자 범위가 (0, 10)을 유지하고 하나의 스레드가 setLower (5)를 호출하고 다른 스레드가 setUpper (4)를 호출하는 경우, 운이 좋지 않은 타이밍이 있으면 둘 다 setter의 검사를 통과하고 두 수정이 모두 적용됩니다. 결과적으로 범위는 이제 유효하지 않은 상태 (5, 4)를 유지합니다. 따라서 기본 AtomicInteger는 스레드로부터 안전하지만 복합 클래스는 그렇지 않습니다. 상한과 하한에 개별 AtomicInteger를 사용하는 대신 AtomicReference를 사용하여이 문제를 해결할 수 있습니다.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}

2
이 기사는 귀하의 답변과 비슷하지만 더 복잡한 것에 깊숙이 들어갑니다. 흥미 롭군! ibm.com/developerworks/java/library/j-jtp04186
LppEdd

20

낙관적 잠금을 적용 할 때 AtomicReference를 사용할 수 있습니다. 공유 객체가 있고 둘 이상의 스레드에서 변경하려고합니다.

  1. 공유 객체의 사본을 만들 수 있습니다
  2. 공유 객체 수정
  3. 공유 객체가 여전히 이전과 동일한 지 확인해야합니다. 그렇다면, 수정 된 사본을 참조하여 업데이트하십시오.

다른 스레드가 수정 했으므로이 두 단계 사이에서 수정할 수 있습니다. 원자 작업에서 수행해야합니다. 이것이 AtomicReference가 도울 수있는 곳입니다


7

다음은 매우 간단한 사용 사례이며 스레드 안전과 관련이 없습니다.

람다 호출간에 객체를 공유하려면AtomicReference 다음 옵션이 있습니다 .

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

나는 이것이 좋은 디자인이나 아무것도 아니라고 말하지는 않지만 (단순한 예일뿐입니다) 람다 호출간에 객체를 공유 해야하는 경우가 있습니다 AtomicReference.

실제로 참조를 보유한 모든 객체를 사용할 수 있으며, 항목이 하나만있는 Collection도 있습니다. 그러나 AtomicReference는 완벽하게 맞습니다.


6

많이 말하지 않겠습니다. 이미 존경받는 동료 친구들이 귀중한 의견을 제시했습니다. 이 블로그 마지막에 본격적인 실행 코드가 있으면 혼동을 제거해야합니다. 멀티 스레드 시나리오에서 영화 좌석 예약 소규모 프로그램에 관한 것입니다.

몇 가지 중요한 기본 사실은 다음과 같습니다. 1> 다른 스레드는 힙 공간에서 예를 들어 정적 멤버 변수에만 경합 할 수 있습니다. 2> 휘발성 읽기 또는 쓰기는 완전히 원자화되고 직렬화 / 합판되어 메모리에서만 수행됩니다. 이것을 말하는 것은 모든 읽기가 메모리의 이전 쓰기를 따른다는 것을 의미합니다. 모든 쓰기는 메모리에서 이전 읽기를 따릅니다. 따라서 휘발성으로 작업하는 스레드는 항상 최신 값을 볼 수 있습니다. AtomicReference는이 휘발성 속성을 사용합니다.

다음은 AtomicReference의 소스 코드 중 일부입니다. AtomicReference는 객체 참조를 나타냅니다. 이 참조는 다음과 같이 AtomicReference 인스턴스의 휘발성 멤버 변수입니다.

private volatile V value;

get ()은 단순히 변수의 최신 값을 반환합니다 (휘발성은 "이전에 발생").

public final V get()

다음은 AtomicReference의 가장 중요한 방법입니다.

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

compareAndSet (expect, update) 메소드는 안전하지 않은 Java 클래스의 compareAndSwapObject () 메소드를 호출합니다. 안전하지 않은이 메소드 호출은 기본 호출을 호출하여 프로세서에 대한 단일 명령을 호출합니다. "예상"및 "업데이트"는 각각 개체를 참조합니다.

AtomicReference 인스턴스 멤버 변수 "value"가 동일한 오브젝트를 "expect"로 참조하는 경우에만 "update"가이 인스턴스 변수에 지정되고 "true"가 리턴됩니다. 그렇지 않으면 false가 반환됩니다. 모든 것은 원자 적으로 이루어집니다. 다른 스레드는 사이를 가로 챌 수 없습니다. 이것은 단일 프로세서 작업 (현대 컴퓨터 아키텍처의 마술)이므로 동기화 된 블록을 사용하는 것보다 빠릅니다. 그러나 여러 변수를 원자 적으로 업데이트해야하는 경우 AtomicReference가 도움이되지 않습니다.

이클립스에서 실행할 수있는 본격적인 실행 코드를 추가하고 싶습니다. 많은 혼란이 사라질 것입니다. 여기 22 명의 사용자 (MyTh 스레드)가 20 개의 좌석을 예약하려고합니다. 다음은 코드 스 니펫과 전체 코드입니다.

22 명의 사용자가 20 개의 좌석을 예약하려고하는 코드 스 니펫.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

다음은 전체 실행 코드입니다.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    

5

언제 AtomicReference를 사용합니까?

AtomicReference 는 동기화를 사용하지 않고 변수 값을 원자 적으로 업데이트하는 유연한 방법입니다.

AtomicReference 단일 변수에서 잠금없는 스레드 안전 프로그래밍을 지원합니다.

높은 수준의 동시 API로 스레드 안전성을 달성하는 방법에는 여러 가지가 있습니다 . 원자 변수는 여러 옵션 중 하나입니다.

Lock 객체는 많은 동시 응용 프로그램을 단순화하는 잠금 관용구를 지원합니다.

Executors스레드 시작 및 관리를위한 고급 API를 정의합니다. java.util.concurrent가 제공하는 실행기 구현은 대규모 애플리케이션에 적합한 스레드 풀 관리를 제공합니다.

동시 수집을 통해 대규모 데이터 수집을보다 쉽게 ​​관리 할 수 ​​있으며 동기화 필요성을 크게 줄일 수 있습니다.

원자 변수 에는 동기화를 최소화하고 메모리 일관성 오류를 방지하는 기능이 있습니다.

AtomicReference를 사용해야하는 간단한 예를 제공하십시오.

샘플 코드 AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

모든 멀티 스레드 프로그램에서 객체를 생성해야합니까?

AtomicReference모든 멀티 스레드 프로그램에서 사용할 필요는 없습니다 .

단일 변수를 보호하려면을 사용하십시오 AtomicReference. 코드 블록을 보호하려면 Lock/ synchronizedetc 와 같은 다른 구문을 사용하십시오 .


-1

또 다른 간단한 예는 세션 객체에서 안전한 스레드 수정을 수행하는 것입니다.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

소스 : http://www.ibm.com/developerworks/library/j-jtp09238/index.html

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.