[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를 수행하려면 ll
and sc
: load-linked 및 store-conditional 을 사용하는 소프트웨어 루프를 작성해야합니다 . Load-linked는 단어를 읽고, store-conditional은 단어가 변경되지 않은 경우 새 값을 저장합니다. 그렇지 않으면 실패 (감지되어 재시도 발생)가 발생합니다.
답변
i++
두 가지 작업이 포함됩니다.
- 현재 값 읽기
i
- 값을 증가시키고 할당
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 더하기
- 메모리에 다시 쓰기
동기화가 없다면 Thread 1이 값 3을 읽고 4로 증가 시켰지만 다시 쓰지 않았다고 가정 해 보겠습니다. 이 시점에서 컨텍스트 전환이 발생합니다. 스레드 2는 값 3을 읽고 증가시키고 컨텍스트 전환이 발생합니다. 두 스레드 모두 총 값을 증가 시켰지만 여전히 4-경쟁 조건입니다.
답변
i++
다음과 같은 세 가지 작업 만 포함하는 명령문입니다.
- 현재 값 읽기
- 새로운 가치 쓰기
- 새로운 가치 저장
이 세 가지 작업은 단일 단계에서 실행되는 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
및 다른 언어 원자 클래스.