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

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();
    }
}



답변

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은 단어가 변경되지 않은 경우 새 값을 저장합니다. 그렇지 않으면 실패 (감지되어 재시도 발생)가 발생합니다.


답변

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


답변

중요한 것은 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 참조 )이 있습니다.


답변

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

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

스레드 1 및 2 :

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

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


답변

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


답변

JVM에서 증분은 읽기와 쓰기를 포함하므로 원 자성이 아닙니다.


답변

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

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

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

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

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

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