Java는 휘발성 키워드 (스레드 원 자성, 가시성, 질서 성)를 학습하도록 안내합니다.

이 키워드에 대해 이야기하지 말고 먼저 질문을하겠습니다. 스레드를 알고 있습니다.

시계

원 자성

온화   

 

이 세 가지 특별한 기능이 있습니까?

 

이해가 안 되시면 아래를 계속 읽으세요

 

Java의 각 스레드가 작동 할 때 고유 한 작업 메모리가 있으며이 작업 메모리는 스레드 만 액세스하고 다른 스레드는 액세스 할 수 없습니다.

각 스레드가 데이터를 읽고 쓸 때 먼저 주 메모리의 데이터를 자체 작업 메모리로 복사 한 다음 스레드가 데이터를 수정하기 위해 읽고 써야 할 때 먼저 자체 작업 메모리에서 읽고 씁니다. 그런 다음 데이터를 주 메모리로 플러시합니다. 그러나 언제 주 메모리로 플러시됩니까? 아무도 당신에게 말할 수 없습니다 ...

관련된 요소가 너무 많기 때문에

 

============== [ 가시성 ] =============

현재 다음과 같은 현상이 있습니다.

주 메모리에는 isExit 속성이 있습니다.

스레드 1은 사본을 자체 작업 메모리로 읽습니다.

스레드 2는 또한 변수 isExit의 데이터를 작업 메모리에 복사하고 변수 isExit의 값을 변경합니다.

예를 들어, 다음 코드 :

    private boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

isExit에 실시간 응답이 필요하지 않으면 괜찮습니다. 값을 수정해야하는 경우 다른 스레드가 즉시 알고 최신 값을 새로 고쳐야하는 경우 문제가 있습니다.

뭐? ? ? ? 왜?

 

스레드 2가 isExit 값을 수정하기 때문에 스레드 2가 주 메모리로 플러시되는시기가 불확실합니다.

스레드 2가 값을 수정하기 전에 스레드 1이 이미 isExit (즉 isExit = false)을 자체 작업 메모리에 복사 한 경우 스레드 1은 주 메모리로 이동하지 않습니다.

이 값을 읽으십시오. 따라서 스레드 1의 값은 항상 isExit = false입니다. 따라서 스레드 1의 실행 기능이 실행되었습니다. 스레드 2가 isExit의 값을 변경했지만.

 

이것은 보이지 않는  스레드이며 스레드가 다른 스레드의 값을 변경하면 즉시 알 수 없습니다.

 

이 문제를 해결하기 위해, 자바가 제공하는   키워드 휘발성을  .만큼 수정 속성 에 의해  휘발성이  수정되고 모든 스레드는 즉시에 대해 배우고 메인 메모리에 다시 읽은 다음 자신의 작업 메모리에 새로 고쳐집니다. 예를 들어, isExit에 volatile  을 사용하도록 위 코드를 수정합니다.  

    private volatile boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

 수정 isExit 을 통해 휘발성  . 일단 스레드 2 isExit의 값을 변경 한 즉시 메인 메모리에서 읽고 자신의 작업 메모리에 isExit에 새로 고침, 그것에 대해 배울 것 스레드. 따라서 volatile로    수정 된   변수    스레드 가시성을 갖습니다. 

============== [ 원 자성 ] ==============

자, 원 자성이 무엇인지 살펴 보겠습니다. 지금은 얘기하지 않고 이전 코드를 살펴 보겠습니다.

    private volatile int num = 0;//累加的变量
    private int count = 0;//完成线程累加
    private int THREAD_COUNT = 20;//线程数目

    public void textVo() {
        //开启20个线程
        for (int x = 0; x < THREAD_COUNT; x++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        num++;
                    }
                    threadOK();
                }
            });
            thread.start();
        }

    }

    private void threadOK() {
        count++;
        //等待所有线程都执行完累加
        if (count == THREAD_COUNT) {
            Log.d(TAG, "num : " + num);
        }
    }

 

코드를 보면 매우 간단합니다. 20 개의 스레드를 시작하면 각 스레드는 변수 num에 10000을 더합니다. 사실은 20 * 10000, 결과는 200000이어야합니다.

좋습니다. 출력을 보여 드리겠습니다.

 

확률은 우리가 기대하는 숫자보다 작습니다. ? ? ? ? 장난 치냐 ????

왜 이런가요? 자, 분석해 보겠습니다.

사실 문제는 num ++ 줄에 나타납니다. 우리 모두는 Java 언어가 마침내 어셈블리 언어로 컴파일 된 다음 어셈블리 명령에 의해 작동된다는 것을 알고 있습니다.

num ++ 줄의 코드도 예외는 아닙니다. 결국 실행을 위해 N 개의 어셈블리 명령어로 컴파일되고 아마도 디 컴파일 된 어셈블리 명령어로 컴파일됩니다.

 

		Code:
		Stack=2,Locals=0,Args_size=0,
		0:	getstatic  #98;//
		3:	iconsts_1
		4:  iadd
		5:  putstatic  #98
		8:  return

 

전체 프로세스는 아마도 어셈블리 지침에 있습니다.

 

1 : 먼저 데이터를 읽고이 값을 작업 스택의 맨 위에 놓습니다 (0 행).

2 : 데이터 축적 (4 행)

3 : 누적 된 데이터 반환 (8 행)

 

 

그렇다면이 시점에서 문제가있을 것입니다.

예 :

1 : 내 쓰레드 1은 100과 같은 num의 값을 읽어서이 값을 오퍼레이션 스택의 맨 위에 놓는다. 이때 다른 쓰레드가 CPU 실행력을 빼앗는다.

2 : Thread 2는 이때 CPU의 실행 권한을 잡고 100과 같은 num의 값을 읽고 누적 후 num에 101을 반환합니다.이 때 num = 101

3 : 그런 다음 이번에는 CPU 실행 권한이 스레드 1에 의해 다시 스내치됩니다. 스레드 1의 스택 맨 위에있는 값은 여전히 ​​100이고 값 클래스가 추가되어 101을 얻고 값이 num으로 반환됩니다.

4 : 그러면 문제는 매우 분명합니다 .Thread 1과 Thread 2는 한 번 누적되고 최종 값은 1 만 증가합니다.

 

그러나, num이  volatile     에 의해 수정되고 라인 2가 수정 된 후 왜 스레드 1이 즉시 인식하고 새 데이터를 읽기 위해 메인 메모리로 이동하지 않는지 알 수 있습니다. 스레드 가시성이 있다는 의미가 아닙니까?

 

이 질문을하는 것은 좋지만 어셈블리 명령이 값 읽기를 연산 스택의 맨 위에 놓고 다른 스레드에서 변수 num을 조작하는 방법은 그와 관련이 없다고 말하는 것은 잔인합니다. 이후 누적 연산 명령은 원본 스택의 맨 위에있는 모든 데이터입니다.

 

그래서 이것은 스레드의  원 자성입니다. 

 

Java 명령어를 실행할 때 가상 머신 인 Atomicity  는 다른 스레드 CPU에서 즉시 수행하기 전에 스레드    가 Java 명령어 완료해야 함을  보장 할 수 있습니다. 참고 그것이라고 자바 명령  ,

예를 들어, num ++과 같은 간단한 자바 코드 줄,

분명히, volatile    키워드로 수정 된 변수는 사용할 수 없습니다. 즉, num 변수에는 스레드 원 자성이 없습니다.

num ++도 있기 때문에 이러한 간단한 자동 증가 명령은 실행되기 전에 다른 스레드에 의해 실행된다는 것을 보장하지 않습니다.

사실, 다른 관점에서 스레드의 컴퓨팅 기능이 원 자성을 보장 할 수 없다고 생각할 수 있습니다.

 

============== [ 질서 성 ] =============

좋아요, 마지막으로 질서에 대해 이야기하겠습니다.

Java 가상 머신의 명령을 실행하는 동작에 대해 이야기하고 마지막 데모에 대해 이야기하겠습니다.

    int a = 0;//第1行
    int b = 1;//第2行
    int c = 2;//第3行
    private void test() {
        a++;      //第4行
        b++;      //第5行
        c = a + b;//第6行
    }

단지 몇 줄의 코드 일뿐입니다. 가상 머신은 보장되지 않습니다.

4 행은 확실히 5 행보다 먼저 실행될 것입니다. 가상 머신이 컴파일 된 후 Java를 다시 최적화하고 정렬합니다.

유일한 보장 : 실행이 코드의 6 번째 줄에 도달하면 4 번째와 5 번째 줄이 실행되어야합니다.

즉, 최종 결과 출력이 올바른지 만 보장합니다. 실행 순서가 작성한 내용과 일치한다는 보장은 없습니다.

음, 휘발성  수정 없이 DoubleLock 싱글 톤 모드가 어떻게되는지 보겠습니다. 

 

이제 휘발성이  없는 마지막  싱글 톤 모드, 표준 DoubleLock 싱글 톤 모드,

public class ThreadDemo {

    private static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

volatile을  추가하지 않으면  스레드 안전 문제가 발생합니다. 추가하지 않는 이유는 무엇입니까?

실제로 스레드 안전성은 코드 9 번째 줄에 나타나며 실제로 9 번째 코드 줄은 기본적으로 실행되는 세 단계로 나뉩니다.

1 : 힙 메모리의 메모리 공간을 ThreadDemo의 인스턴스에 할당합니다 (즉, new ThreadDemo () ).

2 : 메모리 인스턴스 초기화 ( ThreadDemo 생성자를 호출 하여 인스턴스 초기화)

3 : 내부 메모리에 threadDemo 변수를 생성하고 1 단계에서 생성 한 주소를 가리 킵니다 ( 이 단계가 실행되는 한 threadDemo는 비어 있지 않습니다 ).

 

분명히 한  줄의 threadDemo = new ThreadDemo ()   는 3 단계로 나뉩니다.

더 중요한 것은 방금 말했듯이 기본 순서에서도 컴파일러가 코드 실행 순서를 최적화 할 수 있다는 것입니다.

코드 최적화 후 순서는 1-2-3   이거나 1-3-2 일 수 있습니다.

그러면 1-2-3은 괜찮을 것 입니다.

 

컴파일러가 실행 순서를 1-3-2로 변경 하면 어떤 문제가 발생하는지 살펴 보겠습니다 .

장면을 구성합니다.

스레드 1이 먼저 나오고 코드의 9 번째 줄을 먼저 실행하고 1-3을 실행 한 후 threadDemo는 이미 비어 있지 않습니다. 이때 CPU 실행 능력은 스레드 2에 의해 제거됩니다.

이때 스레드 2는 CPU 실행 권한을 획득하고 싱글 톤을 획득하기 위해 들어 와서 threadDemo가 비어 있지 않다고 판단하고 직접 가져와 사용합니다.

 

이것은 문제입니다. 현재 threadDemo는 단순히 메모리 주소를 가리 키지 만이 주소에 저장된 데이터는 아직 초기화되지 않았습니다. 이 예제에는 널 포인터 등과 같은 예기치 않은 문제가 많이 있습니다 ..........

 

따라서이 문제를 해결하기 위해 Java는 키워드 volatile을  제공합니다  .이 키워드로 수정 된 변수는 컴파일러에 다음과 같이 알려줍니다.

이 변수 앞의 코드는 먼저 컴파일러에 의해 실행되어야합니다.

이 변수 뒤의 코드는 컴파일러가 보증 한 후에 실행되어야합니다.

발음하기 어렵습니까? 이해하기 어렵습니까?

몇 줄의 코드를 작성합니다.

volatile int a = 1;
int b = 1;
int c = 1;
int b = 1;

private void test(){
    b++;//第7行
    c++;//第8行

    a++;//第9行

    b = c+b;//第11行
    c = b+;10//第12行

}

 

변수 a는 volatile  수정 되고  컴파일러는

7 행과 8 행의 코드는 9 행보다 먼저 실행해야합니다.

11 행과 12 행의 코드는 9 행 이후에 실행되어야합니다.

그러나 7 행과 8 행에서 어떤 행이 먼저 실행 될지는 보장되지 않습니다.

물론 11 번째 줄과 12 번째 줄이 먼저 실행될 것이라는 보장은 없습니다.

 

휘발성   수정 변수는 구분선과 동일합니다. 앞의 코드는 자신보다 먼저 실행해야하며 다음 코드는 자기 후에 실행해야합니다.

 

이제 단일 예제 모드로 돌아가 보겠습니다. threadDemo 변수 를  volatile로   수정하면 1 단계와 2 단계가 3 단계 전에 실행되어야하므로 컴파일 후 순서는 1-2-3이어야합니다. 따라서 threadDemo가 메모리 주소를 가리킨 후에는 힙 메모리 주소의 데이터가 초기화되어야합니다. 따라서 스레드 안전 문제가 없습니다.

 

따라서 올바른 DoubleLock 싱글 톤 모드는 다음과 같아야합니다.

public class ThreadDemo {

    private volatile static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

 

그래서 우리는 요약합니다 :

수정 변수 와  휘발성   ,

스레드 원 자성 없음

    스레드 가시성

    스레드 순서 있음

 

 

위 코드에는 문제가 없습니다. 수정을 위해 메시지를 남겨주세요. . . 감사합니다

 

 

 

 

 

 

 

 

 

 

 

 

 

추천

출처blog.csdn.net/Leo_Liang_jie/article/details/91465477