단일 모드에서의 게으름뱅이 및 라인 보안 문제

12982 단어
먼저 코드 보기:
package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {//1:  instance  
            instance = new Singleton2();//2:    instance
        }
        return instance;
    }

}
package com.roocon.thread.t5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadMain {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(20);
        for (int i = 0; i< 20; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+Singleton2.getInstance());
                }
            });
        }
     threadPool.shutdown();
} }

실행 결과:
pool-1-thread-4:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-14:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-10:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-8:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-5:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-12:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-1:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-9:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-6:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-2:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-16:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-3:com.roocon.thread.t5.Singleton2@1c208db1
pool-1-thread-17:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-13:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-18:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-7:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-20:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-11:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-15:com.roocon.thread.t5.Singleton2@6519891a
pool-1-thread-19:com.roocon.thread.t5.Singleton2@6519891a

발견하다Singleton2@1c208db1되돌아온 것은 같은 실례가 아니라는 뜻이다.이것이 바로 이른바 라인 안전 문제다.
설명 원인: 상기 코드 주석 부분에 대해 만약에 두 개의 라인이 있다면 라인 A는 1까지 실행하고 instance를null로 읽은 후에 cpu는 라인 B에 의해 빼앗겼다. 이때 라인 A는 instance를 실례화하지 않았다.
따라서 루틴 B가 instance를 읽을 때null로 되어 있기 때문에 instance를 실례화합니다.그리고 cpu는 라인 A에 의해 빼앗겼다.이 때, 스레드 A는 instance의 값을 읽고 null라고 생각하기 때문에,
instance를 다시 실례화하다.따라서 스레드 A와 스레드 B가 되돌아오는 것은 같은 실례가 아니다.
 
그럼 어떻게 해결할까요?
1. 방법 앞에 synchronized 수식을 넣는다.이렇게 하면 틀림없이 다시는 라인 안전 문제가 없을 것이다.
package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
        if(instance == null) {//1
            instance = new Singleton2();//2
        }
        return instance;
    }

}

그러나 이런 해결 방식은 만약에 100개의 라인이 동시에 실행된다면 getInstance 방법을 실행할 때마다 먼저 자물쇠를 얻은 다음에 실행 방법체를 가져와야 한다. 자물쇠가 없으면 기다려야 하고 시간이 오래 걸려서 직렬 처리가 된 것 같다.따라서 다른 더 좋은 처리 방식을 시도해 보자.
2. 코드 블록을 동기화하여 자물쇠의 입자 크기를 줄인다.우리는 첫 번째 instance가null일 때만 실례를 만들고 instance가null인지 아닌지를 판단하는 것은 읽는 작업이기 때문에 루틴 안전 문제가 존재할 수 없다는 것을 발견했다. 따라서 우리는 실례를 만드는 코드에 대해 동기화 코드 블록을 처리해야 한다. 즉, 루틴 안전이 발생할 수 있는 코드에 대해 동기화 코드 블록을 처리해야 한다는 뜻이다.
package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                instance = new Singleton2();
            }
        }
        return instance;
    }

}

그런데 이렇게 처리하면 문제 없나요?같은 원리로 스레드 A와 스레드 B는 스레드 A가 instance 값을null로 읽는다. 이때 cpu는 스레드 B에 의해 빼앗기고 스레드 B는 다시instance 값을null로 판단한다. 그래서 동기화 코드 블록의 코드를 실행하고 instance를 실례화하기 시작한다.이 때, 스레드 A는 cpu를 얻습니다. 스레드 A가 instance 값이null로 판단되었기 때문에, 그 뒤에 있는 동기화 코드 블록 코드를 실행하기 시작합니다.그것 또한 instance를 실례화할 것이다.
이렇게 하면 두 개의 다른 실례를 만들 수 있다.
그렇다면 위의 문제를 어떻게 해결할 것인가.
간단하게 동기화 코드 블록에서 instance가 실례화되기 전에 판단하고 instance가null이면 이를 실례화합니다.이렇게 하면 instance가 한 번만 실례화될 수 있다는 것을 보장할 수 있다.이른바 이중 검사 잠금 메커니즘이다.
위 장면을 다시 분석합니다.
스레드 A와 스레드 B, 스레드 A는 instance 값을null로 읽습니다. 이때 cpu는 스레드 B에 의해 빼앗겼습니다. 스레드 B는 다시 instance 값을null로 판단합니다.그래서 동기화 코드 블록 코드를 실행하기 시작했고 instance를 실례화했다.이것은 스레드 A가 cpu 실행권을 얻은 것이다. 스레드 A가 동기화 코드 블록의 코드를 실행할 때 instance의 값을 판단한다. 스레드 B가 실행된 후에 이 공유 자원인stance를 실례화했기 때문에 instance는null이 아니기 때문에 스레드 A는 다시 실례화 코드를 실행하지 않을 것이다.
package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

}

그러나 이중 검사에 자물쇠를 채우는 것은 코드가 100% 틀림없이 라인 안전에 문제가 없을 것이다.왜냐하면, 여기에는 지령 재배열 문제가 관련되기 때문이다.instance = new Singleton2 ()는 다음 단계로 나눌 수 있습니다.
1. 메모리 공간을 신청한다.
2. 이 공간에서 실례화 대상;
3. instance의 인용은 이 공간 주소를 가리킨다.
명령 순서재정리의 문제점은 다음과 같습니다.
상기 절차에 대해 지령 재배열은 위 123단계에 따라 순서대로 집행되지 않을 가능성이 높다.예를 들어 1을 실행하여 메모리 공간을 신청한 다음에 3단계를 실행하면 instance의 인용은 방금 신청한 메모리 공간 주소를 가리킨다. 그러면 2단계를 수행하고 instance를 판단할 때 instance가 이미 특정한 주소를 가리키기 때문에 instance는null이 되지 않기 때문에 실례화 대상도 되지 않는다.이른바 지령 재배열 안전 문제다.그렇다면 어떻게 이 문제를 해결합니까?
volatile 키워드를 추가합니다. 왜냐하면volatile는 명령의 재배열을 금지할 수 있기 때문입니다.
package com.roocon.thread.t5;

public class Singleton2 {

    private Singleton2(){

    }

    private static volatile Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class){
                if (instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

}

좋은 웹페이지 즐겨찾기