스레드간에 정적 변수가 공유됩니까?


95

스레딩에 관한 상위 Java 클래스의 선생님이 제가 확신 할 수없는 말을했습니다.

그는 다음 코드가 반드시 ready변수를 업데이트하는 것은 아니라고 말했습니다 . 그에 따르면 두 스레드가 반드시 정적 변수를 ReaderThread공유하는 것은 아닙니다. 다른 것을 업데이트하지 않습니다.

기본적으로 그는 ready메인 스레드에서 업데이트 될 수 있지만 에서는 업데이트되지 ReaderThread않으므로 ReaderThread무한 반복됩니다.

그는 또한 프로그램이 0또는 42. 어떻게 42인쇄 할 수 있는지 이해 하지만 0. 그는 이것이 number변수가 기본값으로 설정된 경우라고 언급했습니다 .

스레드간에 정적 변수가 업데이트된다는 것이 보장되지 않을 수도 있다고 생각했지만 이것은 Java에 대해 매우 이상합니다. ready휘발성으로 만들면 이 문제가 해결됩니까?

그는 다음 코드를 보여주었습니다.

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}

비 로컬 변수의 가시성은 이들이 정적 변수, 객체 필드 또는 배열 요소인지 여부에 의존하지 않으며 모두 동일한 고려 사항을 갖습니다. (배열 요소를 휘발성으로 만들 수 없다는 문제가 있습니다.)
Paŭlo Ebermann

1
선생님에게 '0'을 볼 수 있다고 생각하는 건축의 종류를 물어보십시오. 그러나 이론상 그는 옳습니다.
bestsss

4
@bestsss 그런 종류의 질문을하는 것은 그가 말한 내용의 전체 요점을 놓쳤다는 것을 교사에게 드러 낼 것입니다. 요점은 유능한 프로그래머는 보장되지 않는 것과 보장되지 않는 것을 이해하고 보장되지 않는 것에 의존하지 않으며 적어도 보장되지 않는 것과 그 이유를 정확하게 이해하지 않고서는 안된다는 것입니다.
David Schwartz

동일한 클래스 로더에 의해로드 된 모든 항목간에 공유됩니다. 스레드 포함.
Marquis of Lorne

당신의 선생님 (그리고 받아 들여진 대답)은 100 % 옳지 만, 거의 발생하지 않는다는 것을 언급 할 것입니다. 이것은 몇 년 동안 숨겨 질 문제이며 가장 해로울 때만 나타납니다. 문제를 드러내려는 짧은 테스트조차도 모든 것이 정상인 것처럼 행동하는 경향이 있습니다 (아마도 JVM이 많은 최적화를 수행 할 시간이 없기 때문일 것입니다).
Bill K

답변:


75

가시성과 관련하여 정적 변수에 특별한 것은 없습니다. 액세스 할 수있는 경우 모든 스레드가 접근 할 수 있으므로 더 많이 노출되기 때문에 동시성 문제가 발생할 가능성이 더 큽니다.

JVM의 메모리 모델로 인한 가시성 문제가 있습니다. 다음은 메모리 모델과 쓰기가 스레드에 어떻게 표시되는지에 대한 기사 입니다. 한 스레드가 적시에 다른 스레드에 표시되는 변경 사항을 믿을 수 없습니다 (실제로 JVM은 발생 전 관계 를 설정하지 않는 한 이러한 변경 사항을 모든 시간 프레임에 표시 할 의무가 없습니다). .

다음은 해당 링크의 인용문입니다 (Jed Wesley-Smith의 의견에 제공됨).

Java 언어 사양의 17 장은 공유 변수의 읽기 및 쓰기와 같은 메모리 작업에 대한 사전 발생 관계를 정의합니다. 한 스레드의 쓰기 결과는 읽기 작업 전에 쓰기 작업이 발생하는 경우에만 다른 스레드의 읽기에서 볼 수 있습니다. Thread.start () 및 Thread.join () 메서드뿐만 아니라 동기화 및 휘발성 구성은 발생 전 관계를 형성 할 수 있습니다. 특히:

  • 스레드의 각 작업은 프로그램 순서에서 나중에 나오는 해당 스레드의 모든 작업보다 먼저 발생합니다.

  • 모니터의 잠금 해제 (동기화 된 블록 또는 메소드 종료)는 동일한 모니터의 모든 후속 잠금 (동기화 된 블록 또는 메소드 항목) 전에 발생합니다. 그리고 전 발생 관계는 전이 적이므로 잠금 해제 이전 스레드의 모든 작업은 해당 모니터의 스레드 잠금 이후의 모든 작업보다 먼저 발생합니다.

  • 휘발성 필드에 대한 쓰기는 동일한 필드의 모든 후속 읽기 전에 발생합니다. 휘발성 필드의 쓰기 및 읽기는 모니터 진입 및 종료와 유사한 메모리 일관성 효과를 갖지만 상호 배제 잠금을 수반하지 않습니다.

  • 스레드에서 시작하기위한 호출은 시작된 스레드에서 작업을 수행하기 전에 발생합니다.

  • 스레드의 모든 작업은 다른 스레드가 해당 스레드의 조인에서 성공적으로 반환되기 전에 발생합니다.


3
실제로 "적시에"와 "모든"은 동의어입니다. 위의 코드가 절대 종료되지 않을 가능성이 매우 높습니다.
TREE

4
또한 이것은 또 다른 안티 패턴을 보여줍니다. 둘 이상의 공유 상태를 보호하기 위해 휘발성을 사용하지 마십시오. 여기서 number와 ready는 두 가지 상태이며 둘 다 일관되게 업데이트 / 읽기 위해서는 실제 동기화가 필요합니다.
TREE

5
결국 눈에 띄는 부분은 잘못되었습니다. 명시 발생-전에없이 관계를 어떤 쓰기 것이라는 보장이 없다 으로 JIT는 권리가 레지스터에 읽기를 떠맡 기다하고 다음 어떤 업데이 트를 볼 수 없을거야 아주 내에있는 다른 스레드에 의해 볼 수는. 궁극적 인로드는 운이 좋으며 의존해서는 안됩니다.
Jed Wesley-Smith

2
"휘발성 키워드를 사용하거나 동기화하지 않는 한." "작성자와 독자 사이에 관련된 사전 발생 관계가없는 경우"와이 링크를 읽어야합니다. download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith

2
@bestsss 잘 발견되었습니다. 불행히도 ThreadGroup은 여러면에서 깨졌습니다.
Jed Wesley-Smith

37

그는 가시성 에 대해 이야기하고 있었고 너무 문자 그대로 받아 들여서는 안됩니다.

정적 변수는 실제로 스레드간에 공유되지만 한 스레드에서 변경된 내용이 다른 스레드에 즉시 표시되지 않을 수 있으므로 변수의 복사본이 두 개있는 것처럼 보입니다.

이 문서는 그가 정보를 제공 한 방법과 일치하는보기를 제공합니다.

먼저 Java 메모리 모델에 대해 약간 이해해야합니다. 나는 그것을 간단하고 잘 설명하기 위해 수년에 걸쳐 조금 고생했습니다. 오늘 현재 제가 생각할 수있는 가장 좋은 방법은 다음과 같이 상상하는 것입니다.

  • Java의 각 스레드는 별도의 메모리 공간에서 발생합니다 (이것은 분명히 사실이 아니므로이 부분에 대해 저를 참고하십시오).

  • 메시지 전달 시스템에서와 같이 이러한 스레드간에 통신이 발생하도록 보장하려면 특수 메커니즘을 사용해야합니다.

  • 한 스레드에서 발생하는 메모리 쓰기는 "누출"되어 다른 스레드에서 볼 수 있지만 이것이 보장되는 것은 아닙니다. 명시 적 의사 소통 없이는 다른 스레드에서 어떤 쓰기가 표시되는지 또는 표시되는 순서를 보장 할 수 없습니다.

...

스레드 모델

그러나 이것은 문자 그대로 JVM이 작동하는 방식이 아니라 스레딩과 휘발성에 대해 생각하는 단순한 정신 모델입니다.


12

기본적으로 사실이지만 실제로 문제는 더 복잡합니다. 공유 데이터의 가시성은 CPU 캐시뿐만 아니라 비 순차적 명령 실행의 영향을받을 수 있습니다.

따라서 Java는 스레드가 공유 데이터의 일관된 상태를 볼 수있는 상황을 나타내는 메모리 모델을 정의합니다 .

특정 경우에 추가하면 volatile가시성이 보장됩니다.


8

물론 둘 다 동일한 변수를 참조한다는 점에서 "공유"되지만 반드시 서로의 업데이트를 볼 필요는 없습니다. 이것은 정적뿐만 아니라 모든 변수에 적용됩니다.

이론적으로는 변수가 선언 volatile되거나 쓰기가 명시 적으로 동기화 되지 않는 한 다른 스레드에서 작성한 쓰기가 다른 순서로 나타날 수 있습니다 .


4

단일 클래스 로더 내에서 정적 필드는 항상 공유됩니다. 데이터 범위를 스레드로 명시 적으로 지정하려면 ThreadLocal.


2

정적 기본 유형 변수를 초기화 할 때 Java 기본값은 정적 변수에 대한 값을 지정합니다.

public static int i ;

이와 같이 변수를 정의 할 때 기본값은 i = 0입니다. 그래서 0을 얻을 가능성이 있습니다. 그러면 메인 스레드가 부울 값을 true로 업데이트합니다. ready는 정적 변수이기 때문에 주 스레드와 다른 스레드는 동일한 메모리 주소를 참조하므로 준비 변수가 변경됩니다. 그래서 보조 스레드는 while 루프에서 빠져 나와 값을 인쇄합니다. 인쇄 할 때 number의 초기화 값은 0입니다. 스레드 프로세스가 주 스레드 업데이트 번호 변수 전에 while 루프를 통과 한 경우. 그러면 0을 인쇄 할 수 있습니다.


-2

@dontocsata 선생님에게 돌아가서 조금 학교에 갈 수 있습니다 :)

실제 세계에서 몇 가지 메모를보고 무엇을 보거나 듣든지 상관없이. 아래의 단어는 표시된 정확한 순서로이 특정 경우에 관한 것입니다.

다음 2 개의 변수는 거의 모든 알려진 아키텍처에서 동일한 캐시 라인에 상주합니다.

private static boolean ready;  
private static int number;  

Thread.exit(메인 스레드)는 exit스레드 그룹 스레드 제거 (및 기타 여러 문제)로 인해 종료되도록 보장되고 메모리 펜스를 유발할 수 있습니다. (동기화 된 호출이며, 데몬 스레드가 남아 있지 않으면 ThreadGroup도 종료되어야하므로 동기화 부분없이 구현할 단일 방법이 없습니다.)

시작된 스레드 ReaderThread는 데몬이 아니기 때문에 프로세스를 유지합니다! 따라서 readynumber함께 플러시됩니다 (또는 컨텍스트 전환이 발생하는 경우 이전 번호)이 경우 재정렬에 대한 실제 이유가 없습니다. 적어도 하나는 생각할 수 없습니다. 하지만 뭔가를 보려면 정말 이상한 것이 필요합니다 42. 다시 한 번 두 정적 변수가 동일한 캐시 라인에 있다고 가정합니다. 4 바이트 길이의 캐시 라인이나 연속 영역 (캐시 라인)에 할당하지 않는 JVM을 상상할 수 없습니다.


3
@bestsss는 오늘날 모두 사실이지만 프로그램의 의미가 아니라 현재의 JVM 구현 및 하드웨어 아키텍처에 의존합니다. 이것은 프로그램이 작동하더라도 여전히 손상되었음을 의미합니다. 지정된 방식으로 실제로 실패하는이 예제의 사소한 변형을 쉽게 찾을 수 있습니다.
Jed Wesley-Smith

1
나는 그것이 사양을 따르지 않는다고 말했지만, 교사로서 적어도 일부 상품 아키텍처에서 실제로 실패 할 수있는 적절한 예를 찾아야하므로 그 예는 일종의 실제입니다.
bestsss

6
내가 본 스레드로부터 안전한 코드 작성에 대한 최악의 조언 일 수 있습니다.
Lawrence Dol

4
@Bestsss : 귀하의 질문에 대한 간단한 대답은 "특정 시스템 또는 구현의 부작용이 아니라 사양 및 문서에 대한 코드"입니다. 이는 기본 하드웨어와 무관하게 설계된 가상 머신 플랫폼에서 특히 중요합니다.
Lawrence Dol

1
@Bestsss : 교사의 요점은 (a) 코드를 테스트 할 때 잘 작동 할 수 있고 (b) 사양의 보증이 아니라 하드웨어 작동에 의존하기 때문에 코드가 손상된다는 것입니다. 요점은 괜찮아 보이지만 괜찮지 않다는 것입니다.
Lawrence Dol
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.