i ++가 원 자성이 아닌 이유는 무엇입니까?


97

i++Java에서 원자가 아닌 이유는 무엇 입니까?

Java를 좀 더 깊이 이해하기 위해 스레드의 루프가 실행되는 빈도를 세려고했습니다.

그래서 나는

private static int total = 0;

메인 클래스에서.

두 개의 스레드가 있습니다.

  • 글타래 (쓰레드) 1 : Prints System.out.println("Hello from Thread 1!");
  • 스레드 2 : Prints System.out.println("Hello from Thread 2!");

그리고 스레드 1과 스레드 2에 의해 인쇄 된 줄을 계산합니다. 그러나 스레드 1의 줄 + 스레드 2의 줄은 인쇄 된 총 줄 수와 일치하지 않습니다.

내 코드는 다음과 같습니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static int total = 0;
    private static int countT1 = 0;
    private static int countT2 = 0;
    private boolean run = true;

    public Test() {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        newCachedThreadPool.execute(t1);
        newCachedThreadPool.execute(t2);
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        run = false;
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println((countT1 + countT2 + " == " + total));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT1++;
                System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT2++;
                System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}

14
시도해 보지 그래 AtomicInteger?
Braj 2014-08-06


3
JVM에는 iinc정수를 증가 시키는 작업이 있지만 동시성이 문제가되지 않는 지역 변수에 대해서만 작동합니다. 필드의 경우 컴파일러는 읽기-수정-쓰기 명령을 별도로 생성합니다.
Silly Freak 2014 년

14
왜 그것이 원자적일 것이라고 기대합니까?
Hot Licks 2014-08-06

2
@Silly Freak : iinc필드 에 대한 명령어 가 있더라도 단일 명령어를 사용한다고해서 원 자성을 보장하지 않습니다. 예를 들어 단일 바이트 코드 명령어에 의해 수행된다는 사실과 관계없이 원 자성이 보장되지 않는 필드 액세스 volatile long및 비 double.
Holger

답변:


125

i++원자 성은 대부분의 .NET Framework 사용에없는 특수 요구 사항이기 때문에 Java에서는 원 자성이 아닐 수 i++있습니다. 이 요구 사항에는 상당한 오버 헤드가 있습니다. 증분 작업을 원자 단위로 만드는 데 많은 비용이 듭니다. 일반적인 증분으로 존재할 필요가없는 소프트웨어 및 하드웨어 수준의 동기화가 포함됩니다.

i++비원 자적 증가가를 사용하여 수행되도록 특별히 원자 적 증가를 수행하는 것으로 설계 및 문서화되어야 하는 인수를 만들 수 i = i + 1있습니다. 그러나 이것은 Java와 C 및 C ++ 간의 "문화적 호환성"을 깨뜨릴 것입니다. 또한 C와 유사한 언어에 익숙한 프로그래머가 당연하게 여기는 편리한 표기법을 제거하여 제한된 상황에서만 적용되는 특별한 의미를 부여합니다.

기본 C 또는 C ++ 코드는 다음과 같이 for (i = 0; i < LIMIT; i++)Java로 변환됩니다 for (i = 0; i < LIMIT; i = i + 1). atomic을 사용하는 것은 부적절하기 때문입니다 i++. 더 나쁜 것은 C 또는 다른 C와 유사한 언어에서 Java로 온 프로그래머가 i++어쨌든 사용하여 원자 명령어를 불필요하게 사용하게 된다는 것 입니다.

기계 명령어 세트 수준에서도 증가 유형 작업은 일반적으로 성능상의 이유로 원자 적이 지 않습니다. x86에서는 inc위와 같은 이유로 명령어를 원자 단위 로 만들기 위해 특수 명령어 "잠금 접두사"를 사용해야합니다 . 경우 inc비 원자 INC이 필요한 경우가 사용하지 않을 것, 항상 원자이었다; 프로그래머와 컴파일러는로드, 1을 추가하고 저장하는 코드를 생성 할 것입니다.

일부 명령 집합 아키텍처에는 원자가 inc없거나 전혀 없습니다 inc. MIPS에서 atomic inc를 수행하려면 lland sc: load-linked 및 store-conditional 을 사용하는 소프트웨어 루프를 작성해야합니다 . Load-linked는 단어를 읽고, store-conditional은 단어가 변경되지 않은 경우 새 값을 저장합니다. 그렇지 않으면 실패 (감지되어 재시도 발생)가 발생합니다.


2
자바에는 포인터가 없기 때문에 지역 변수를 증가시키는 것은 본질적으로 스레드 저장이므로 루프를 사용하면 문제가 대부분 그렇게 나쁘지 않습니다. 당연히 최소한의 놀라움을 의미합니다. 그대로도, i = i + 1대한 번역 것 ++i,하지i++
바보 괴물

22
질문의 첫 번째 단어는 "왜"입니다. 현재로서는 이것이 "왜"문제를 해결하는 유일한 답입니다. 다른 답변은 실제로 질문을 다시 설명합니다. 그래서 +1.
Dawood ibn Kareem 2014 년

3
원 자성 보장은 비 volatile필드 업데이트에 대한 가시성 문제를 해결하지 못한다는 점에 주목할 가치가 있습니다. 따라서 volatile한 스레드가 ++연산자를 사용한 후에 모든 필드를 암시 적으로 취급하지 않는 한 이러한 원 자성 보장은 동시 업데이트 문제를 해결하지 못합니다. 그렇다면 문제가 해결되지 않으면 잠재적으로 성능을 낭비하는 이유는 무엇입니까?
Holger

1
@DavidWallace 의미하지 ++않습니까? ;)
Dan Hlavenka 2014-08-14

36

i++ 두 가지 작업이 포함됩니다.

  1. 현재 값 읽기 i
  2. 값을 증가시키고 할당 i

두 스레드 i++가 동시에 동일한 변수에 대해 수행 할 때 둘 다 동일한 현재 값을 얻은 i다음 증분하고로 설정 i+1하므로 두 개 대신 단일 증분을 얻을 수 있습니다.

예 :

int i = 5;
Thread 1 : i++;
           // reads value 5
Thread 2 : i++;
           // reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
           // i == 6 instead of 7

(경우에도 i++ 이었다 원자, 그것은 잘 정의되지 않을 것이다 / 스레드 안전 행동.)
user2864740

15
+1,하지만 "1. A, 2. B 및 C"는 2 개가 아니라 3 개 연산처럼 들립니다. :)
yshavit 2014-08-06

3
작업이 저장 위치를 ​​증가시키는 단일 기계 명령어로 구현 된 경우에도 스레드로부터 안전하다는 보장은 없습니다. 기계는 여전히 값을 가져 오기를 증가, 다시 저장할 필요가 플러스 가 저장 위치의 여러 캐시 사본이있을 수 있습니다.
Hot Licks 2014-08-07

3
@Aquarelle-두 프로세서가 동일한 저장 위치에 대해 동일한 작업을 동시에 실행하고 해당 위치에 "예약"브로드 캐스트가없는 경우 거의 확실하게 간섭하여 가짜 결과를 생성합니다. 예,이 작업은 "안전"할 수 있지만 하드웨어 수준에서도 특별한 노력이 필요합니다.
Hot Licks 2014-08-07

6
하지만 질문은 "무슨 일이 일어나는지"가 아니라 "왜"라고 생각합니다.
Sebastian Mach

11

중요한 것은 JVM의 다양한 구현이 언어의 특정 기능을 구현했는지 여부가 아니라 JLS (Java Language Specification)입니다. JLS는 ia "값 1이 변수의 값에 추가되고 합계가 변수에 다시 저장된다"라고 말하는 15.14.2 절에서 ++ 접미사 연산자를 정의합니다. 멀티 스레딩이나 원자성에 대해 언급하거나 암시하는 곳은 없습니다. 이를 위해 JLS는 휘발성동기화 된 . 또한 패키지 java.util.concurrent.atomic ( http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html 참조 )이 있습니다.


5

Java에서 i ++가 원 자성이 아닌 이유는 무엇입니까?

증분 연산을 여러 문으로 나눕니다.

스레드 1 및 2 :

  1. 메모리에서 총계 값 가져 오기
  2. 값에 1 더하기
  3. 메모리에 다시 쓰기

동기화가 없다면 Thread 1이 값 3을 읽고 4로 증가 시켰지만 다시 쓰지 않았다고 가정 해 보겠습니다. 이 시점에서 컨텍스트 전환이 발생합니다. 스레드 2는 값 3을 읽고 증가시키고 컨텍스트 전환이 발생합니다. 두 스레드 모두 총 값을 증가 시켰지만 여전히 4-경쟁 조건입니다.


2
나는 이것이 어떻게 질문에 대한 답이되어야하는지 이해하지 못한다. 언어는 증분이든 유니콘이든 모든 기능을 원자로 정의 할 수 있습니다. 당신은 원자 적이 지 않은 결과를 예시 할뿐입니다.
Sebastian Mach

예, 언어는 모든 기능을 원자로 정의 할 수 있지만 java가 증가 연산자로 간주되는 한 (OP가 게시 한 질문) 원자가 아니며 내 대답에 이유가 나와 있습니다.
Aniket Thakur 2014-08-07

1
(첫 번째 코멘트의 거친 어조로 미안합니다) 그러나 그 이유는 "원자 적이라면 경쟁 조건이 없기 때문"인 것 같습니다. 즉, 경쟁 조건이 바람직한 것처럼 들립니다.
Sebastian Mach

@phresnel 원자 적 증가를 유지하기 위해 도입 된 오버 헤드는 거대하고 거의 바람직하지 않으므로 작업을 저렴하게 유지하고 결과적으로 원 자성이 아닌 것이 대부분의 경우 바람직합니다.
josefx

4
@josefx : 사실에 의문을 제기하는 것이 아니라이 답변의 추론에 유의하십시오. 그것은 기본적으로 말한다 "내가 ++ 자바 때문에이 가지고있는 경쟁 조건의 원자 아니다" 말처럼, "자동차 때문에 일어날 수있는 충돌의 어떤 에어백이 없습니다" 또는 "는 이유로 당신의 쿠리 부어 스트 - 주문이 더 칼을 얻을 wurst를 잘라야 할 수도 있습니다 . " 따라서 나는 이것이 답이라고 생각하지 않습니다. 질문은 "i ++는 무엇을합니까?" 가 아닙니다. 또는 "i ++가 동기화되지 않은 결과는 무엇입니까?" .
Sebastian Mach

5

i++ 다음과 같은 세 가지 작업 만 포함하는 명령문입니다.

  1. 현재 값 읽기
  2. 새로운 가치 쓰기
  3. 새로운 가치 저장

이 세 가지 작업은 단일 단계에서 실행되는 i++것이 아니며 즉 , 복합 작업 이 아닙니다 . 결과적으로 하나 이상의 스레드가 복합적이지 않은 단일 작업에 포함되면 모든 종류의 문제가 발생할 수 있습니다.

다음 시나리오를 고려하십시오.

시간 1 :

Thread A fetches i
Thread B fetches i

시간 2 :

Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i

// At this time thread B seems to be more 'active'. Not only does it overwrite 
// its local copy of i but also makes it in time to store -bar- back to 
// 'main' memory (i)

시간 3 :

Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
value (in i) which was just stored by thread B in Time 2.

Thread B has nothing to do here. Its work was done by Time 2. However it was 
all for nothing as -bar- was eventually overwritten by another thread.

그리고 거기에 있습니다. 경쟁 조건.


그것이 i++원자가 아닌 이유 입니다. 만약 그렇다면, 이런 일은 일어나지 않았을 것이고 각각 fetch-update-store은 원자 적으로 일어날 것입니다. 그것이 바로 그 AtomicInteger목적이며 귀하의 경우에는 아마도 적합 할 것입니다.

추신

이 모든 문제를 다루는 훌륭한 책은 다음과 같습니다. Java Concurrency in Practice


1
흠. 언어는 증분이든 유니콘이든 모든 기능을 원자로 정의 할 수 있습니다. 당신은 원자 적이 지 않은 결과를 예시 할뿐입니다.
Sebastian Mach

@phresnel 정확합니다. 그러나 나는 또한 단일 작업이 아니라는 점을 지적합니다. 확장 적으로 이러한 여러 작업을 원자 단위로 전환하는 데 필요한 계산 비용이 훨씬 더 비싸다는 것을 의미하며, 이는 부분적으로 i++원 자성이 아닌 이유를 정당화 합니다.
kstratis 2014-08-07

1
내가 귀하의 요점을 이해하는 동안 귀하의 대답은 학습에 약간 혼란 스럽습니다. 나는 예와 "예의 상황 때문에"라는 결론을 봅니다. 이럴이 불완전한 이유는 :(이다
세바스찬 마하

1
@phresnel 아마도 가장 교육적인 대답은 아니지만 제가 현재 제공 할 수있는 최선의 방법입니다. 사람들을 돕고 혼동하지 않기를 바랍니다. 그러나 비판에 감사드립니다. 나는 앞으로의 게시물에서 더 정확하도록 노력할 것입니다.
kstratis


2

작업 i++이 원자 적이면 값을 읽을 기회가 없습니다. 이것은 i++(를 사용하는 대신) 사용하여 수행하려는 작업 ++i입니다.

예를 들어 다음 코드를보십시오.

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

이 경우 출력은 다음과 같을 것으로 예상합니다. 0 (왜냐하면 증분을 게시하기 때문입니다. 예를 들어 먼저 읽은 다음 업데이트합니다.)

이것이 값을 읽고 (그리고 그것으로 무언가를 수행) 값 업데이트 해야하기 때문에 작업이 원자적일 수없는 이유 중 하나입니다 .

다른 중요한 이유는 원자 적 으로 무언가를 수행하는 데 일반적 으로 잠금으로 인해 더 많은 시간이 소요 된다는 것 입니다. 사람들이 원자 적 연산을 원할 때 드물게 원시에 대한 모든 연산이 조금 더 오래 걸리는 것은 어리석은 일입니다. 그들은 추가 한 이유입니다 AtomicInteger다른 언어 원자 클래스.


2
이것은 오해의 소지가 있습니다. 당신은 그렇지 않으면 당신은에서 값을 가져올 수 없습니다, 실행하고 결과를 얻기를 분리 할 필요가 있는 원자 조작.
Sebastian Mach

그렇지 않습니다. Java의 AtomicInteger에 get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () 등이 있습니다.
Roy van Rijn

1
그리고 자바 언어를 정의 할 수 i++로 확장 할 i.getAndIncrement(). 이러한 확장은 새로운 것이 아닙니다. 예를 들어 C ++의 람다는 C ++의 익명 클래스 정의로 확장됩니다.
Sebastian Mach

원자가 주어지면 i++사소하게 원자를 만들 수 있으며 ++i그 반대의 경우도 마찬가지입니다. 하나는 다른 하나를 더한 것과 같습니다.
David Schwartz 2015

2

두 단계가 있습니다.

  1. 기억에서 나를 가져와
  2. i + 1을 i로 설정

원자 적 연산이 아닙니다. thread1이 i ++를 실행하고 thread2가 i ++를 실행하면 i의 최종 값은 i + 1이 될 수 있습니다.


-1

동시성 ( Thread클래스 등)은 Java v1.0에 추가 된 기능입니다 .i++그 전에 베타에 추가되었으며, 원래 구현에서 여전히 가능성이 높습니다.

변수를 동기화하는 것은 프로그래머에게 달려 있습니다. 이에 대한 Oracle의 자습서를 확인하십시오. .

편집 : 명확히하기 위해, i ++는 Java 이전에 잘 정의 된 프로 시저이므로 Java 설계자는 해당 프로 시저의 원래 기능을 유지하기로 결정했습니다.

++ 연산자는 Java와 스레딩보다 약간 앞서는 B (1969)에서 정의되었습니다.


-1 "public class Thread ... Since : JDK1.0"출처 : docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak

버전은 여전히 ​​Thread 클래스 이전에 구현되었으며 변경되지 않았기 때문에 그다지 중요하지 않지만 귀하를 기쁘게하기 위해 제 답변을 편집했습니다.
TheBat 2014-08-06

5
중요한 것은 "Thread 클래스 이전에 여전히 구현되었다"는 주장이 소스에 의해 뒷받침되지 않는다는 것입니다. i++원자 적이 지 않는 것은 설계 결정이지 성장하는 시스템에 대한 감독이 아닙니다.
Silly Freak 2014 년

귀엽 네요. Java 이전에 존재했던 언어가 있었기 때문에 i ++는 Threads 이전에 정의되었습니다. Java의 제작자는 잘 받아 들여진 절차를 재정의하는 대신 이러한 다른 언어를 기반으로 사용했습니다. 내가 어디에서 감독이라고 말했습니까?
TheBat

@SillyFreak 다음은 ++가 얼마나 오래되었는지 보여주는 몇 가지 소스입니다. en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.