복합 작업 i++
은 여러 작업 을 포함하므로 스레드 안전하지 않다는 것을 알고 있습니다.
그러나 참조 자체를 스레드 안전 작동으로 확인합니까?
a != a //is this thread-safe
이것을 프로그래밍하고 여러 스레드를 사용하려고했지만 실패하지 않았습니다. 내 컴퓨터에서 레이스를 시뮬레이션 할 수 없었습니다.
편집하다:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
이것은 스레드 안전성을 테스트하는 데 사용하는 프로그램입니다.
이상한 행동
내 프로그램이 일부 반복 사이에서 시작되면 출력 플래그 값을 얻습니다. 즉 !=
, 동일한 참조 에서 참조 검사가 실패합니다. 그러나 일부 반복 후 출력은 일정한 값이 false
되고 프로그램을 오랫동안 실행해도 단일 true
출력이 생성되지 않습니다 .
출력은 일부 n (고정되지 않은) 반복 후에 제안되는 것처럼 출력은 일정한 값으로 보이고 변경되지 않습니다.
산출:
일부 반복의 경우 :
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
답변
동기화가없는 경우이 코드
Object a;
public boolean test() {
return a != a;
}
생산할 수 있습니다 true
. 이것은 바이트 코드입니다test()
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
IF_ACMPEQ L1
...
우리가 필드 a
를 로컬 var에 두 번 로드하는 것을 볼 수 있듯이 a
다른 스레드 비교에 의해 사이에 변경 된 경우 비 원자 연산 false
입니다.
또한 메모리 가시성 문제는 여기서 관련 a
이 있으므로 다른 스레드에서 변경 한 내용 이 현재 스레드에 표시 될 것이라는 보장은 없습니다 .
답변
점검은
a != a
안전합니까?
a
올바른 동기화없이 다른 스레드가 잠재적으로 업데이트 할 수있는 경우 아니요
이것을 프로그래밍하고 여러 스레드를 사용하려고했지만 실패하지 않았습니다. 내 컴퓨터에서 레이스를 시뮬레이션 할 수 없었습니다.
그것은 아무 의미가 없습니다! 문제는 a
다른 스레드에 의해 업데이트 된 실행 이 JLS에 의해 허용 되는 경우 코드가 스레드로부터 안전하지 않다는 것입니다. 특정 시스템 및 특정 Java 구현에서 특정 테스트 케이스로 경쟁 조건을 발생시킬 수 없다고해서 다른 상황에서 발생하는 것을 막을 수는 없습니다.
! = A 반환 할 수있는 것을이 의미합니까
true
.
이론적으로 특정 상황에서는 가능합니다.
또는 동시에 변경 되어도 a != a
돌아올 수 있습니다.false
a
“이상한 행동”에 관하여 :
내 프로그램이 일부 반복 사이에서 시작되면 출력 플래그 값을 얻습니다. 즉, 동일한 참조에서 참조! = 검사가 실패 함을 의미합니다. 그러나 일부 반복 후 출력은 상수 값 false가되고 프로그램을 오랫동안 실행해도 단일 실제 출력이 생성되지 않습니다.
이 “이상한”동작은 다음 실행 시나리오와 일치합니다.
-
프로그램이로드되고 JVM이 바이트 코드 해석을 시작 합니다. (javap 출력에서 볼 수 있듯이) 바이트 코드는 두 가지로드를 수행하기 때문에 경쟁 조건의 결과를 때때로 볼 수 있습니다.
-
얼마 후 코드는 JIT 컴파일러에 의해 컴파일됩니다. JIT 옵티마이 저는 동일한 메모리 슬롯 (
a
) 의 두로드가 서로 가까이 있음 을 확인하고 두 번째로드를 최적화합니다. (실제로 테스트를 완전히 최적화 할 가능성이 있습니다 …) -
이제 더 이상 두 개의로드가 없기 때문에 경쟁 조건이 더 이상 나타나지 않습니다.
이것은 모두 JLS가 Java 구현을 허용하는 것과 일치합니다.
@kriss는 다음과 같이 논평했다.
이것은 C 또는 C ++ 프로그래머가 “정의되지 않은 동작”(구현에 따라 다름)이라고 부르는 것 같습니다. Java와 같은 코너 경우에는 몇 가지 UB가있는 것처럼 보입니다.
Java 메모리 모델 ( JLS 17.4로 지정)은 한 스레드가 다른 스레드가 쓴 메모리 값을 볼 수 있도록하는 일련의 사전 조건을 지정합니다. 하나의 스레드가 다른 스레드에 의해 작성된 변수를 읽으려고 시도하고 해당 전제 조건이 충족되지 않으면 여러 실행이 가능할 수 있습니다 (일부 응용 프로그램 요구 사항의 관점에서) 부정확 할 수 있습니다. 즉, 설정 가능한 행동은 정의 ( “잘 구성된 실행”세트를 즉),하지만 우리는 발생하는 행동의 어느 말할 수 없습니다.
컴파일러는 코드의 최종 효과가 동일한 경우로드를 결합 및 재정렬하고 저장하고 다른 작업을 수행 할 수 있습니다.
- 단일 스레드로 실행될 때
- 메모리 모델에 따라 올바르게 동기화되는 다른 스레드에서 실행될 때.
그러나 코드가 제대로 동기화되지 않아서 “이전에 발생하는”관계가 제대로 구성된 실행 세트를 충분히 제한하지 않는 경우 컴파일러는 “잘못된”결과를 제공하는 방식으로로드 및 저장 순서를 다시 지정할 수 있습니다. (그러나 그것은 단지 프로그램이 틀렸다는 것을 말하는 것입니다.)
답변
test-ng로 입증 :
public class MyTest {
private static Integer count=1;
@Test(threadPoolSize = 1000, invocationCount=10000)
public void test(){
count = new Integer(new Random().nextInt());
Assert.assertFalse(count != count);
}
}
10 000 호출에서 2 개의 실패가 있습니다. 따라서 NO , 스레드 안전 하지 않습니다.
답변
전혀 그렇지 않다. 비교를 위해 Java VM은 스택에서 비교할 두 값을 넣고 비교 명령 ( “a”의 유형에 따라 다름)을 실행해야합니다.
Java VM은 다음을 수행 할 수 있습니다.
- “a”를 두 번 읽고 각각 스택에 놓고 결과를 비교하십시오.
- “a”를 한 번만 읽고 스택에 놓고 복제 ( “dup”명령) 한 다음 비교를 실행하십시오.
- 식을 완전히 제거하고
false
첫 번째 경우에는 다른 스레드가 두 읽기 사이의 “a”값을 수정할 수 있습니다.
어떤 전략을 선택할지는 Java 컴파일러 및 Java 런타임 (특히 JIT 컴파일러)에 따라 다릅니다. 프로그램 실행 중에 변경 될 수도 있습니다.
변수에 액세스하는 방법을 확인하려면 변수를 만들거나 volatile
( “반쯤 메모리 장벽”이라고 함) 전체 메모리 장벽을 추가해야합니다 ( synchronized
). 일부 hgiher 레벨 API를 사용할 수도 있습니다 (예 : AtomicInteger
Juned Ahasan에서 언급 한대로).
스레드 안전성에 대한 자세한 내용은 JSR 133 ( Java Memory Model )을 읽으십시오 .
답변
Stephen C에 의해 모두 잘 설명되어 있습니다. 재미를 위해 다음 JVM 매개 변수를 사용하여 동일한 코드를 실행 해 볼 수 있습니다.
-XX:InlineSmallCode=0
이것은 JIT (핫스팟 7 서버에서 수행)가 수행하는 최적화를 방해해야하며 true
영원히 볼 수 있습니다 (2,000,000에서 멈췄지만 그 후에도 계속한다고 가정합니다).
자세한 내용은 JIT 코드입니다. 솔직히 말해서, 테스트가 실제로 수행되는지 또는 두로드가 어디서 오는지를 알기에 유창하게 어셈블리를 읽지 않습니다. (26 행은 테스트 flag = a != a
이고 31 행은의 닫는 괄호입니다 while(true)
).
# {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
0x00000000027dcc80: int3
0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
0x00000000027dcc8c: data32 data32 xchg ax,ax
0x00000000027dcc90: mov DWORD PTR [rsp-0x6000],eax
0x00000000027dcc97: push rbp
0x00000000027dcc98: sub rsp,0x40
0x00000000027dcc9c: mov rbx,QWORD PTR [rdx+0x8]
0x00000000027dcca0: mov rbp,QWORD PTR [rdx+0x18]
0x00000000027dcca4: mov rcx,rdx
0x00000000027dcca7: movabs r10,0x6e1a7680
0x00000000027dccb1: call r10
0x00000000027dccb4: test rbp,rbp
0x00000000027dccb7: je 0x00000000027dccdd
0x00000000027dccb9: mov r10d,DWORD PTR [rbp+0x8]
0x00000000027dccbd: cmp r10d,0xefc158f4 ; {oop('javaapplication27/TestThreadSafety$1')}
0x00000000027dccc4: jne 0x00000000027dccf1
0x00000000027dccc6: test rbp,rbp
0x00000000027dccc9: je 0x00000000027dcce1
0x00000000027dcccb: cmp r12d,DWORD PTR [rbp+0xc]
0x00000000027dcccf: je 0x00000000027dcce1 ;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
0x00000000027dccd1: add rbx,0x1 ; OopMap{rbp=Oop off=85}
;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
0x00000000027dccd5: test DWORD PTR [rip+0xfffffffffdb53325],eax # 0x0000000000330000
;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
; {poll}
0x00000000027dccdb: jmp 0x00000000027dccd1
0x00000000027dccdd: xor ebp,ebp
0x00000000027dccdf: jmp 0x00000000027dccc6
0x00000000027dcce1: mov edx,0xffffff86
0x00000000027dcce6: mov QWORD PTR [rsp+0x20],rbx
0x00000000027dcceb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=112}
;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
; {runtime_call}
0x00000000027dccf0: int3
0x00000000027dccf1: mov edx,0xffffffad
0x00000000027dccf6: mov QWORD PTR [rsp+0x20],rbx
0x00000000027dccfb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=128}
;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
; {runtime_call}
0x00000000027dcd00: int3 ;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
0x00000000027dcd01: int3
답변
아니요, a != a
스레드 안전하지 않습니다. 이 표현식은 load a
, a
다시 로드 및 수행 의 세 부분으로 구성됩니다 !=
. 다른 스레드가 a
의 부모 에 대한 본질적인 잠금을 획득 a
하고 2 개의로드 조작 사이에서 값을 변경할 수 있습니다.
그러나 다른 요소 a
는 로컬 인지 여부 입니다. a
로컬 인 경우 다른 스레드에 액세스 할 수 없어 스레드 안전해야합니다.
void method () {
int a = 0;
System.out.println(a != a);
}
항상 인쇄해야합니다 false
.
선언 a
으로하는 volatile
경우에 대한 문제가 해결되지 것 a
입니다 static
또는 인스턴스를. 문제는 스레드의 값이 다르지 a
않지만 하나 의 스레드가 다른 값으로 a
두 번 로드된다는 것 입니다. 실제로 만들 수 있습니다 경우 더 적은 스레드 안전 .. 만약이 a
되지 volatile
다음 a
캐시 할 수 있으며, 다른 스레드의 변화가 캐시 된 값에 영향을 미치지 않습니다.
답변
이상한 행동에 대해 :
변수 a
가로 표시되지 않았기 때문에 volatile
어느 시점 a
에서 스레드에 의해 값 이 캐시 될 수 있습니다. 모두 a
의의는 a != a
다음 캐시 된 버전 및 따라서 항상 같은 (의미는 flag
항상 지금이다 false
).