! = 점검 스레드는 안전한가요?


140

복합 작업 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

2
이 문맥에서 "스레드 안전"이란 무엇입니까? 항상 거짓을 반환한다고 보장하고 있습니까?
JB 니 제트

@JBNizet 예. 그것이 내가 생각한 것입니다.
Narendra Pathai

5
단일 스레드 컨텍스트에서 항상 false를 반환하지는 않습니다. 그것은 NaN이 될 수있다 ..
해롤드

4
가능한 설명 : 코드가 적시에 컴파일되었고 컴파일 된 코드는 변수를 한 번만로드합니다. 이것은 예상됩니다.
Marko Topolnik

3
개별 결과를 인쇄하는 것은 경주를 테스트하기에 좋지 않은 방법입니다. 인쇄 (결과 형식화 및 결과 작성)는 테스트와 비교하여 비용이 많이 듭니다 (때로는 터미널 또는 터미널 자체에 대한 연결 대역폭이 느린 경우 프로그램이 쓰기시 블로킹 될 수 있습니다). 또한 IO에는 종종 스레드의 실행 순서를 방해하는 자체 뮤텍스가 포함되어 있습니다 (개별 라인이 서로1234:true 스매시하지 마십시오 ). 레이스 테스트에는 더 단단한 내부 루프가 필요합니다. 마지막에 요약을 인쇄하십시오 (아래에서 누군가가 단위 테스트 프레임 워크로 수행 한 것처럼).
벤 잭슨

답변:


124

동기화가없는 경우이 코드

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이 있으므로 다른 스레드에서 변경 한 내용 이 현재 스레드에 표시 될 것이라는 보장은 없습니다 .


22
강력한 증거이지만 바이트 코드는 실제로 증거가 아닙니다. JLS 어딘가에 있어야합니다 ...
Marko Topolnik

10
@Marko 나는 당신의 생각에 동의하지만 반드시 당신의 결론은 아닙니다. 나에게 위의 바이트 코드 !=는 LHS와 RHS를 별도로로드하는 명백하고 정식적인 구현 방법입니다 . 따라서 LLS와 RHS가 구문 상 동일 할 때 JLS 최적화에 대해 구체적으로 언급 하지 않으면 일반적인 규칙이 적용됩니다. 이는 a두 번 로드를 의미 합니다.
Andrzej Doyle

20
실제로, 생성 된 바이트 코드가 JLS를 준수한다고 가정하면 증명입니다!
proskor

6
@Adrian : 첫 번째 : 해당 가정이 유효하지 않더라도 "true"로 평가할 수 있는 단일 컴파일러 의 존재 는 때때로 "true"로 평가 될 수 있음을 입증하기에 충분합니다 (사양이 금지 된 경우에도 해당). 하지 않습니다). 둘째 : Java는 잘 지정되어 있으며 대부분의 컴파일러는 Java와 밀접하게 호환됩니다. 이러한 점에서 참조로 사용하는 것이 좋습니다. 셋째 : "JRE"라는 용어를 사용하지만 이것이 의미한다고 생각하는 것은 아닙니다. . .
ruakh

2
@AlexanderTorstling- "단일 읽기 최적화를 배제하기에 충분한 지 확실하지 않습니다." 충분하지 않습니다. 실제로, 동기화가 존재하지 않는 (그리고 추가로 발생하는 "일부 발생"관계가없는 경우) 최적화가 유효합니다.
Stephen C

47

점검은 a != a안전합니까?

a올바른 동기화없이 다른 스레드가 잠재적으로 업데이트 할 수있는 경우 아니요

이것을 프로그래밍하고 여러 스레드를 사용하려고했지만 실패하지 않았습니다. 내 컴퓨터에서 레이스를 시뮬레이션 할 수 없었습니다.

그것은 아무 의미가 없습니다! 문제는 a다른 스레드에 의해 업데이트 된 실행 이 JLS에 의해 허용 되는 경우 코드가 스레드로부터 안전하지 않다는 것입니다. 특정 시스템 및 특정 Java 구현에서 특정 테스트 케이스로 경쟁 조건을 발생시킬 수 없다고해서 다른 상황에서 발생하는 것을 막을 수는 없습니다.

! = A 반환 할 수있는 것을이 의미합니까 true.

이론적으로 특정 상황에서는 가능합니다.

또는 동시에 변경 되어도 a != a돌아올 수 있습니다.falsea


"이상한 행동"에 관하여 :

내 프로그램이 일부 반복 사이에서 시작되면 출력 플래그 값을 얻습니다. 즉, 동일한 참조에서 참조! = 검사가 실패 함을 의미합니다. 그러나 일부 반복 후 출력은 상수 값 false가되고 프로그램을 오랫동안 실행해도 단일 실제 출력이 생성되지 않습니다.

이 "이상한"동작은 다음 실행 시나리오와 일치합니다.

  1. 프로그램이로드되고 JVM이 바이트 코드 해석을 시작 합니다. (javap 출력에서 ​​볼 수 있듯이) 바이트 코드는 두 가지로드를 수행하기 때문에 경쟁 조건의 결과를 때때로 볼 수 있습니다.

  2. 얼마 후 코드는 JIT 컴파일러에 의해 컴파일됩니다. JIT 옵티마이 저는 동일한 메모리 슬롯 ( a) 의 두로드가 서로 가까이 있음 을 확인하고 두 번째로드를 최적화합니다. (실제로 테스트를 완전히 최적화 할 가능성이 있습니다 ...)

  3. 이제 더 이상 두 개의로드가 없기 때문에 경쟁 조건이 더 이상 나타나지 않습니다.

이것은 모두 JLS가 Java 구현을 허용하는 것과 일치합니다.


@kriss는 다음과 같이 논평했다.

이것은 C 또는 C ++ 프로그래머가 "정의되지 않은 동작"(구현에 따라 다름)이라고 부르는 것 같습니다. Java와 같은 코너 경우에는 몇 가지 UB가있는 것처럼 보입니다.

Java 메모리 모델 ( JLS 17.4로 지정)은 한 스레드가 다른 스레드가 쓴 메모리 값을 볼 수 있도록하는 일련의 사전 조건을 지정합니다. 하나의 스레드가 다른 스레드에 의해 작성된 변수를 읽으려고 시도하고 해당 전제 조건이 충족되지 않으면 여러 실행이 가능할 수 있습니다 (일부 응용 프로그램 요구 사항의 관점에서) 부정확 할 수 있습니다. 즉, 설정 가능한 행동은 정의 ( "잘 구성된 실행"세트를 즉),하지만 우리는 발생하는 행동의 어느 말할 수 없습니다.

컴파일러는 코드의 최종 효과가 동일한 경우로드를 결합 및 재정렬하고 저장하고 다른 작업을 수행 할 수 있습니다.

  • 단일 스레드로 실행될 때
  • 메모리 모델에 따라 올바르게 동기화되는 다른 스레드에서 실행될 때.

그러나 코드가 제대로 동기화되지 않아서 "이전에 발생하는"관계가 제대로 구성된 실행 세트를 충분히 제한하지 않는 경우 컴파일러는 "잘못된"결과를 제공하는 방식으로로드 및 저장 순서를 다시 지정할 수 있습니다. (그러나 그것은 단지 프로그램이 틀렸다는 것을 말하는 것입니다.)


이것이 사실 a != a을 반환 할 수 있습니까?
proskor

내 컴퓨터에서 위 코드가 스레드가 안전하지 않다는 것을 시뮬레이션 할 수 없다는 것을 의미했습니다. 아마도 그 뒤에 이론적 추론이있을 수 있습니다.
Narendra Pathai

@NarendraPathai-당신이 그것을 설명 할 수없는 이론적 인 이유는 없습니다. 실용적인 이유가있을 수 있습니다 ... 또는 아마도 당신은 운이 좋지 않았을 것입니다.
Stephen C

사용중인 프로그램으로 업데이트 된 답변을 확인하십시오. 수표는 때때로 true를 반환하지만 출력에 이상한 동작이있는 것 같습니다.
Narendra Pathai

1
@ NarendraPathai-내 설명을 참조하십시오.
Stephen C

27

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 , 스레드 안전 하지 않습니다.


6
당신은 평등을 확인조차하지 않습니다 ... Random.nextInt()부분은 불필요합니다. 당신도 new Object()마찬가지로 테스트 할 수 있습니다 .
Marko Topolnik

@MarkoTopolnik 사용중인 프로그램으로 업데이트 된 답변을 확인하십시오. 수표는 때때로 true를 반환하지만 출력에 이상한 동작이있는 것 같습니다.
Narendra Pathai

1
참고로, 임의의 객체는 일반적으로 재사용해야하며 새로운 int가 필요할 때마다 생성되지는 않습니다.
Simon Forsberg

15

전혀 그렇지 않다. 비교를 위해 Java VM은 스택에서 비교할 두 값을 넣고 비교 명령 ( "a"의 유형에 따라 다름)을 실행해야합니다.

Java VM은 다음을 수행 할 수 있습니다.

  1. "a"를 두 번 읽고 각각 스택에 놓고 결과를 비교하십시오.
  2. "a"를 한 번만 읽고 스택에 놓고 복제 ( "dup"명령) 한 다음 비교를 실행하십시오.
  3. 식을 완전히 제거하고 false

첫 번째 경우에는 다른 스레드가 두 읽기 사이의 "a"값을 수정할 수 있습니다.

어떤 전략을 선택할지는 Java 컴파일러 및 Java 런타임 (특히 JIT 컴파일러)에 따라 다릅니다. 프로그램 실행 중에 변경 될 수도 있습니다.

변수에 액세스하는 방법을 확인하려면 변수를 만들거나 volatile( "반쯤 메모리 장벽"이라고 함) 전체 메모리 장벽을 추가해야합니다 ( synchronized). 일부 hgiher 레벨 API를 사용할 수도 있습니다 (예 : AtomicIntegerJuned Ahasan에서 언급 한대로).

스레드 안전성에 대한 자세한 내용은 JSR 133 ( Java Memory Model )을 읽으십시오 .


선언 a으로 volatile여전히 사이에 변화의 가능성과 함께, 서로 다른 두 개의 읽기를 의미하는 것입니다.
Holger

6

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   

1
이것은 무한 루프가 있고 모든 것이 어느 정도 게양 될 수있을 때 JVM이 실제로 생성하는 코드 종류의 좋은 예입니다. 여기서 실제 "루프"는에서 0x27dccd1~ 까지의 세 가지 명령입니다 0x27dccdf. jmp(루프가 무한대이기 때문에) 루프에서 무조건이다. 루프의 다른 두 가지 명령 add rbc, 0x1은 증가 countOfIterations하고 있습니다 (루프가 절대 종료되지 않으므로이 값을 읽을 수는 없지만 디버거에서 침입 할 경우 필요할 수 있음). .
BeeOnRope

... 그리고 이상한 test메모리 명령은 실제로 메모리 액세스에만 있습니다 ( eax메소드에는 설정되지 않았습니다!) : JVM이 모든 스레드를 트리거하려고 할 때 읽을 수없는 특수 페이지입니다 안전한 지점에 도달하기 위해 gc 또는 모든 스레드가 알려진 상태에 있어야하는 다른 작업을 수행 할 수 있습니다.
BeeOnRope

더구나, JVM instance. a != instance.a은 루프 에서 비교를 완전히 수행하고 루프에 들어가기 전에 한 번만 수행합니다! 그것은 더 재 장전에 필요하다는 것을 알고 instance또는 a그들은 휘발성 선언하지 않는 한 그것은 단지 그들이 메모리에 의해 허용되는 전체 루프 동안 동일하다고 가정하므로, 동일한 스레드에서 변경할 수있는 다른 코드가 없다 모델.
BeeOnRope

5

아니요, 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캐시 할 수 있으며, 다른 스레드의 변화가 캐시 된 값에 영향을 미치지 않습니다.


당신의 예가 synchronized잘못되었습니다 : 그 코드가 인쇄되도록 보장하려면 설정 된false 모든 메소드 도 이어야 합니다. asynchronized
ruakh

왜 그래? 메소드가 동기화 된 경우, 메소드를 a실행하는 동안 다른 스레드가 상위를 본질적으로 잠그는 방법은 값을 설정하는 데 필요합니다 a.
DoubleMx2

1
당신의 전제가 잘못되었습니다. 고유 잠금을 획득하지 않고 객체의 필드를 설정할 수 있습니다. Java는 필드를 설정하기 전에 객체의 고유 잠금을 획득하기 위해 스레드가 필요하지 않습니다.
ruakh

3

이상한 행동에 대해 :

변수 a가로 표시되지 않았기 때문에 volatile어느 시점 a에서 스레드에 의해 값 이 캐시 될 수 있습니다. 모두 a의의는 a != a다음 캐시 된 버전 및 따라서 항상 같은 (의미는 flag항상 지금이다 false).


0

간단한 읽기조차 원자 적이 지 않습니다. along있고로 표시되지 않은 경우 volatile32 비트 JVM long b = a은 스레드로부터 안전하지 않습니다.


휘발성과 원자 성은 관련이 없습니다. 내가 변동성을 표시하더라도 그것은 원자 적이 지 않을 것이다
Narendra Pathai

휘발성 장 필드의 할당은 항상 원자 적입니다. ++와 같은 다른 작업은 그렇지 않습니다.
ZhekaKozlov
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.