java 다중 루틴 프로그래밍의 왜 데이터 동기화

Java의 변수는 로컬 변수와 클래스 변수로 나뉩니다.국부 변수는 방법 내에서 정의된 변수, 예를 들어run 방법에서 정의된 변수를 가리킨다.이러한 변수에 대해 말하자면, 라인 간의 공유 문제는 존재하지 않는다.따라서 이들은 데이터 동기화를 할 필요가 없다.클래스 변수는 클래스에 정의된 변수이고 작용역은 전체 클래스입니다.이런 변수는 여러 개의 라인에 공유될 수 있다.따라서 우리는 이런 변수에 대해 데이터 동기화를 해야 한다.데이터 동기화란 같은 시간에 한 라인으로만 동기화된 클래스 변수에 접근할 수 있으며, 현재 라인이 이 변수에 접근한 후에야 다른 라인도 계속 접근할 수 있다.여기서 말하는 접근은 쓰기 작업이 있는 접근을 가리킨다. 만약에 모든 접근 변수의 라인이 읽기 작업이라면 일반적으로 데이터 동기화를 필요로 하지 않는다.그렇다면 공유된 클래스 변수에 데이터 동기화를 하지 않으면 어떤 상황이 일어날까요?다음 코드에서 어떤 일이 일어날지 먼저 봅시다

package test;

public class MyThread extends Thread
{
    public static int n = 0;

    public void run()
    {
        int m = n;
        yield();
        m++;
        n = m;
    }
    public static void main(String[] args) throws Exception
    {
        MyThread myThread = new MyThread ();
        Thread threads[] = new Thread[100];
        for (int i = 0; i < threads.length; i++)
            threads[i] = new Thread(myThread);
        for (int i = 0; i < threads.length; i++)
            threads[i].start();
        for (int i = 0; i < threads.length; i++)
            threads[i].join();
        System.out.println("n = " + MyThread.n);
    }
}

위의 코드를 실행할 수 있는 결과는 다음과 같습니다

n = 59
이 결과를 보면 많은 독자들이 이상하게 느낄 것이다.이 프로그램은 분명히 100개의 라인을 시작한 후에 모든 라인에 정적 변수 n을 1씩 추가합니다.마지막으로join 방법을 사용하여 이 100개의 라인을 모두 실행한 후에 이 n값을 출력합니다.정상적으로 말하면 결과는 n=100일 것이다.하필 100보다 작았어.사실 이런 결과를 낳은 주범은 우리가 자주 언급하는 더러운 데이터다.한편, run 방법의 yield() 문장은 바로'더러운 데이터'가 발생하는 시발용자(yield 문장을 넣지 않아도 더러운 데이터가 생길 수 있지만 이렇게 뚜렷하지 않다. 100을 더 큰 수로 바꿔야'더러운 데이터'가 자주 발생한다. 이 예에서 yield를 호출하는 것은'더러운 데이터'의 효과를 확대하기 위해서이다.yield 방법의 역할은 라인을 멈추게 하는 것이다. 즉, yield 방법을 호출하는 라인을 잠시 CPU 자원을 포기하고 CPU가 다른 라인을 실행할 수 있는 기회를 주는 것이다.이 프로그램이 어떻게'더러운 데이터'를 만드는지 설명하기 위해서, 우리는thread1과thread2 두 개의 라인만 만들었다고 가정합니다.thread1의start 방법을 먼저 호출했기 때문에,thread1의run 방법은 일반적으로 먼저 실행됩니다.thread1의 run 방법이 첫 줄로 실행될 때 (int m = n;)n의 값을 m에 부여합니다.두 번째 줄의 yield 방법을 실행하면thread1은 잠시 실행을 멈추고,thread1이 멈추면thread2는 CPU 자원을 얻은 후 실행을 시작합니다(이전thread2는 준비 상태였습니다),thread2는 첫 번째 줄까지 실행됩니다(intm=n;)때,thread1이 yield에 실행되었을 때 n은 여전히 0이기 때문에,thread2의 m가 얻은 값도 0입니다.이렇게 해서thread1과thread2의 m가 얻은 것은 모두 0이다.그것들이 yield 방법을 실행한 후에 모두 0부터 1을 추가합니다. 따라서 누가 먼저 실행하든지 마지막 n의 값은 1입니다. 단지 이 n은thread1과thread2에 각각 값을 부여했습니다.n++만 있다면'더러운 데이터'가 생길 수 있느냐고 물어볼 수도 있다.답은 긍정적이다.그렇다면 n++는 하나의 문장일 뿐, 어떻게 실행 과정에서 CPU를 다른 라인에 전달합니까?사실 이것은 표면적인 현상일 뿐이다. n++는 자바 컴파일러에 의해 중간 언어(바이트 코드라고도 부른다)로 컴파일된 후에 하나의 언어가 아니다.아래의 자바 코드가 어떤 자바 중간 언어로 컴파일되는지 봅시다..

public void run()
{
    n++;
}
컴파일된 중간 언어 코드

public void run()
{
 aload_0
 dup
 getfield
 iconst_1 
 iadd
 putfield
 return
}
run 방법에는 n++ 한 개의 문장만 있고 컴파일된 후에는 7개의 중간 언어가 있는 것을 볼 수 있습니다.우리는 이 문장들의 기능이 무엇인지 알 필요가 없다. 005, 007, 008행 문장만 보자.005행은 getfield로 영어의 의미에 따라 어떤 값을 얻어야 한다는 것을 알 수 있다. 여기에 n이 하나밖에 없기 때문에 n의 값을 얻어야 한다는 것은 의심할 여지가 없다.007행의iadd도 이것을 얻은 n값에 1을 더한 것으로 추측하기 어렵지 않다.008행의putfield의 의미는 여러분이 이미 알아맞혔을 것입니다. 이것은 이것을 1을 추가한 후에 n을 다시 클래스 변수 n으로 업데이트하는 것을 책임집니다.이쯤 되면 궁금한 게 하나 더 있을 거예요. n++를 실행할 때 n을 1까지 넣으면 되잖아요. 왜 이렇게 우여곡절을 겪어야 하는지.사실 여기에는 자바 메모리 모델의 문제가 관련되어 있다.Java의 메모리 모델은 주 저장소와 작업 저장소로 나뉜다.홈 저장소에는 Java의 모든 인스턴스가 저장됩니다.즉, 우리가 new를 사용하여 대상을 만든 후에 이 대상과 그 내부의 방법, 변수 등은 모두 이 구역에 저장되고 MyThread 클래스의 n은 이 구역에 저장된다.메인 메모리 구역은 모든 라인에 공유될 수 있다.작업 저장 구역은 바로 우리가 앞에서 말한 라인 창고이다. 이 구역에는run방법과run방법이 호출한 방법에 정의된 변수, 즉 방법 변수가 저장되어 있다.스레드가 메인 저장소의 변수를 수정할 때, 이 변수를 직접 수정하는 것이 아니라, 현재 스레드의 작업 저장소로 복사하고, 수정이 끝난 후에 이 변수 값을 메인 저장소의 상응하는 변수 값으로 덮어씁니다.자바의 메모리 모델을 이해한 후에 왜 n++도 원자 조작이 아닌지 이해하기 어렵지 않다.그것은 복사, 더하기 1, 덮어쓰기 과정을 거쳐야 한다.이 프로세스는 MyThread 클래스에서 시뮬레이션하는 프로세스와 유사합니다.getfield를 실행할 때thread1이 어떤 이유로 중단되면 MyThread 클래스의 실행 결과와 유사한 상황이 발생할 것이라고 상상할 수 있습니다.이 문제를 철저히 해결하려면 반드시 어떤 방법을 사용하여 n에 대해 동기화를 해야 한다. 즉, 같은 시간에 하나의 라인만 n을 조작할 수 있다. 이것은 n에 대한 원자 조작이라고도 부른다.

좋은 웹페이지 즐겨찾기