동시 프로그래밍 1: 스레드 안전성 개요

목차

1. 스레드 안전성이란 무엇입니까?

2. 작업의 원자성: 경쟁 조건 방지

3. 잠금 메커니즘: 내장 잠금 및 재진입 가능

4. 상태를 보호하기 위해 잠금을 사용하는 방법은 무엇입니까?

5. 동기화 메커니즘의 활동 및 성능 문제


        스레드로부터 안전한 코드 작성의 핵심은 상태 액세스 작업 , 특히 공유 (Shared)변경 가능한(Mutable) 상태 에 대한 액세스를 관리하는 것입니다. //핵심: 공유 및 변경 가능한 상태 관리

        객체의 상태는 상태 변수에 저장된 데이터를 나타냅니다. 상태 변수는 클래스의 인스턴스 또는 멤버 변수일 수 있습니다.

        객체가 스레드로부터 안전해야 하는지 여부는 여러 스레드에서 액세스하는지 여부에 따라 달라집니다. 객체를 스레드로부터 안전하게 만들려면 객체의 변경 가능한 상태에 대한 액세스를 조정하는 동기화 메커니즘이 필요합니다 . 시너지 효과를 얻지 못하면 데이터 손상 및 기타 바람직하지 않은 결과가 발생할 수 있습니다. //동기화를 통해 객체의 스레드 안전성 확보

        Java의 주요 동기화 메커니즘은 배타적 잠금 방법을 제공하는 동기화된 키워드이며 , 휘발성 유형 변수 , 명시적 잠금 (명시적 잠금) 및 원자 변수 도 포함합니다 . //동기화 시 가시성과 원자성(시퀀스)을 해결합니다.

1. 스레드 안전성이란 무엇입니까?

        여러 스레드에서 액세스할 때 클래스가 항상 올바르게 동작하는 경우 클래스가 스레드로부터 안전하다고 합니다. //당신이 보는 것은 당신이 아는 것이다

public class StatelessFactorizer extends GenericServlet implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        //1-从req中获取值
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        //2-编码并响应
        encodeIntoResponse(resp, factors);
    }
}

        위의 StatelessFactorizer는 상태 비저장입니다. 멤버 변수도 포함하지 않으며 다른 클래스의 멤버 변수에 대한 참조도 포함하지 않습니다 . 계산 중 임시 상태는 스레드 스택의 지역 변수에만 존재하며 실행 스레드에서만 액세스할 수 있습니다. 상태 비저장 객체에 액세스하는 스레드의 동작은 다른 스레드에서 객체 작업의 정확성에 영향을 주지 않으므로 상태 비저장 객체는 스레드로부터 안전합니다 . // 공유하지 않으면 스레드 안전 문제가 없습니다.

2. 작업의 원자성: 경쟁 조건 방지

        처리된 요청 수를 계산하기 위해 Hit Counter를 증가시키고 싶다고 가정해 보겠습니다. 가장 쉬운 방법은 서블릿에 long 유형의 멤버 변수를 추가하고 요청이 처리될 때마다 이 값에 1을 추가하는 것입니다. 코드는 다음과 같습니다.

//存在线程安全问题
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    //计数器
    private long count = 0;
    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        //1-从req中获取值
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        //2-编码并响应
        encodeIntoResponse(resp, factors);
    }
}

        우리 모두는 증가 연산 ++count가 단지 하나의 연산인 것처럼 보이지만 이 연산은 원자적이지 않다는 것을 알고 있습니다. 실제로 이는 count 값 읽기, 값 증가, 결과를 count에 쓰는 세 가지 개별 작업으로 구성됩니다. 이는  "읽기-수정-쓰기" 작업 순서 이며 결과 상태는 이전 상태에 따라 달라집니다 . //명령은 비원자적입니다. 단계가 엉망이면 결과도 엉망이 됩니다.

        위의 UnsafeCountingFactorizer에는 여러 경쟁 조건이 있어 결과를 신뢰할 수 없습니다. 가장 일반적인 경쟁 조건"Check-Then-Act" 작업으로 , 잘못된 관찰을 사용하여 다음 작업을 결정합니다 . // 전제조건이 false이면 인수의 결과는 일반적으로 false입니다.

        예를 들어 먼저 특정 조건이 true인지(예: 파일 X가 존재하지 않음) 관찰한 다음 이 관찰 결과를 기반으로 작업(파일 X 생성)을 수행하지만 실제로는 이 결과를 관찰하고 생성을 시작하는 사이 에 파일을 삭제하면 관찰이 유효하지 않게 되어 (그 동안 다른 스레드가 파일 X를 생성함) 다양한 문제(예기치 않은 비정상적인 데이터 덮어쓰기, 파일 손상 등)를 일으킬 수 있습니다. //싱글톤 문제

        스레드 안전성을 보장하고 경쟁 조건을 방지하려면 "실행 전 확인" 및 "읽기-수정-쓰기" 와 같은 작업이 원자적이어야 합니다 .

public class CountingFactorizer extends GenericServlet implements Servlet {
    //使用原子类
    private final AtomicLong count = new AtomicLong(0);
    public long getCount() { return count.get(); }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(resp, factors);
    }
}

        실제 상황에서는 클래스의 상태를 관리하기 위해 기존 스레드로부터 안전한 객체(예: AcomicLong)를 최대한 많이 사용해야 합니다 . 스레드로부터 안전하지 않은 개체에 비해 스레드로부터 안전한 개체의 상태와 상태 전환을 판단하는 것이 더 쉽기 때문에 스레드 안전을 유지하고 확인하는 것이 더 쉽습니다. // 단일 변수의 보안에만 유효합니다.

3. 잠금 메커니즘: 내장 잠금 및 재진입 가능

        여러 변수에 직면할 때 원자 클래스는 동기화 메커니즘이 효과적이라는 것을 보장하지 않습니다.

//存在线程安全问题
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
    //原子类变量1
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
    //原子类变量2
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        //两个变量不能保证同时获取或者同时设置
        if (i.equals(lastNumber.get())) //获取变量1的值
            encodeIntoResponse(resp, lastFactors.get()); //获取变量2的值
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i); //设置变量1的值
            lastFactors.set(factors); //设置变量2的值
            encodeIntoResponse(resp, factors);
        }
    }
}

이 시점에서는 스레드 동기화를 보장하기 위해 잠금 메커니즘을         도입해야 합니다 .

        Java는 원자성을 지원하는 내장 잠금 메커니즘인 동기화된 블록을 제공합니다 . 각 Java 객체는 동기화를 위해 잠금으로 사용될 수 있으며, 이러한 잠금을 내장 잠금(Intrinsic Lock) 또는 모니터 잠금(Monitor Lock) 이라고 합니다 . //소위 모니터 프로세스인데, 동기화가 너무 흔해서 너무 많이 도입하기는 어렵습니다.

        동기화 문제: 동기화된 코드 블록을 사용하면 코드를 과도하게 보호하기 쉽고 보안 문제는 해결되지만 성능 문제가 발생합니다 . //대략적이고 세분화된 잠금 문제

        내장된 잠금은 재진입이 가능 하므로 스레드가 이미 보유하고 있는 잠금을 획득하려고 시도하면 요청이 성공합니다. "헤비 맨"은 잠금 획득 작업의 세분성이 "콜"이 아닌 "라인"임을 의미합니다 . // 재진입이 불가능하면 자체 차단 문제가 발생합니다.

재진입을 달성하는 한 가지 방법은 각 잠금을 획득 횟수소유자 스레드         와 연결하는 것입니다 . 개수 값이 0이면 어떤 스레드도 잠금을 보유하지 않은 것으로 간주됩니다. 스레드가 보유되지 않은 잠금을 요청하면 JVM은 잠금 보유자를 확인하고 획득 횟수를 1로 설정합니다. 동일한 스레드가 다시 잠금을 획득하면 카운터 값이 증가하고 스레드가 동기화된 코드 블록을 종료할 때 그에 따라 카운터가 감소됩니다. 개수가 0이 되면 잠금이 해제됩니다. //재진입 잠금 구현 원리

4. 상태를 보호하기 위해 잠금을 사용하는 방법은 무엇입니까?

        잠금을 사용하면 보호하는 코드에 순차적으로 액세스할 수 있으므로 잠금을 통해 공유 상태에 대한 독점적인 액세스가 가능합니다.

        다음은 잠금을 올바르게 사용하기 위한 몇 가지 제안 사항입니다.

        (1) 동기화를 사용하여 변수에 대한 액세스를 조정하는 경우 변수가 액세스되고 조작되는 모든 위치에서 동기화를 사용해야 합니다 . 또한 변수에 접근하고 조작하는 모든 곳에 동일한 잠금이 사용됩니다. //공유 변수 읽기 및 쓰기는 잠겨 있어야 합니다.

모든 객체에 기본 잠금이         있는 이유는 명시적으로 잠금 객체를 생성하는 것을 피하기 위해서입니다. 공유 상태에 대한 액세스를 보호하기 위해 자체 잠금 프로토콜이나 동기화 전략을 구성하고 이를 프로그램 전체에서 사용할 수 있습니다.

        (2) 모든 공유 변수와 가변 변수는 하나의 잠금으로만 보호되어야 합니다 . 그래야 관리자가 어떤 잠금인지 알 수 있습니다.

        (3) 여러 변수가 포함된 각 불변 조건의 경우 관련된 모든 변수는 동일한 잠금으로 보호되어야 합니다 .

5. 동기화 메커니즘의 활동 및 성능 문제

        동기화를 통해 경쟁 조건 문제를 피할 수 있다면 모든 메서드 선언에 동기화 키워드를 사용하는 것은 어떨까요?

        실제로 동기화를 무분별하게 사용하게 되면 프로그램 내에서 과도한 동기화가 발생할 수 있습니다 . 또한 각 메서드를 Vector와 같은 동기화된 메서드로 만드는 것만으로는 Vector의 복합 작업이 원자성임을 보장하기에 충분하지 않습니다.

//非原子操作
if (!vector.contains(element))
    vector.add(element);

        또한 각 메서드를 동기식 메서드로 만들면 활성 또는 성능 문제가 발생할 수 있습니다 .

        다음 코드의 경우,SynchronizedFactorizer의 동기화 방법을 사용하면 코드의 실행 성능이 매우 저하됩니다. //메서드를 직접 잠글 수 없음 스레드 안전성은 달성되었으나 성능 대가가 너무 높음

//线程安全
public class SynchronizedFactorizer extends GenericServlet implements Servlet {
    //成员变量
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;

    //直接锁方法,存在性能问题
    public synchronized void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);

        if (i.equals(lastNumber))
            encodeIntoResponse(resp, lastFactors);
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        }
    }
}

        잠금 최적화 아이디어: 동기화 코드 블록의 범위를 좁혀 서블릿의 동시성을 보장하고 동시에 스레드 안전성을 유지합니다. 동기화된 코드 블록이 너무 작지 않은지 확인하고 원자성이어야 하는 작업을 여러 동기화된 코드 블록으로 분할하지 마십시오. 공유 상태에 영향을 주지 않는 더 긴 작업은 동기화된 코드 블록에서 분리되어야 합니다. 그래야 다른 스레드가 이러한 작업을 실행하는 동안 공유 상태에 액세스할 수 있습니다. //조잡한 잠금을 최대한 최소화하고, 실행 시간이 긴 코드를 제거합니다.

        재구성된 CachedFactorizer는 단순성과 동시성 사이의 균형을 달성합니다. 코드는 아래와 같이 표시됩니다.

//线程安全
public class CachedFactorizer extends GenericServlet implements Servlet {
    //共享变量
    private BigInteger   lastNumber;
    private BigInteger[] lastFactors;
    //命中计数器
    private long         hits;
    //cache命中计数器
    private long         cacheHits;

    public synchronized long getHits() {
        return hits;
    }

    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        //1-从req获取值
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null; 
        synchronized (this) { //同步代码块1,对变量进行操作
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);   //局部变量,不需要进行同步
            synchronized (this) {  //同步代码块2,对变量进行操作
                lastNumber  = i;
                lastFactors = factors.clone();
            }
        }
        //2-响应:把执行时间长的代码进行剥离
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

        AtomicLong 유형의 적중 카운터는 더 이상 CachedFactorizer에서 사용되지 않지만 long 유형의 변수가 사용됩니다. 물론 단일 변수에 대한 원자적 연산을 구현하는 데 매우 유용한 AtomicLong 유형을 사용할 수도 있습니다. 그러나 여기서는 이미 동기화된 코드 블록을 사용하여 원자 연산을 구성했기 때문에 두 가지 다른 동기화 메커니즘을 사용하면 혼란을 초래할 뿐만 아니라 성능이나 보안에 어떤 이점도 가져오지 않으므로 여기서는 원자 변수를 사용하지 않겠습니다 . //동일한 클래스에서는 코드를 간단하고 이해하기 쉽게 만들기 위해 하나의 동기화 메커니즘만 사용해야 합니다.

        동기화된 코드 블록의 합리적인 크기를 결정하려면 안전성(충족되어야 함), 단순성 및 성능을 포함한 다양한 설계 요구 사항 간의 절충이 필요합니다. 때로는 단순성과 성능 사이에 충돌이 있지만 일반적으로 둘 사이에는 합리적인 균형이 있습니다 . 단순성과 성능 사이에는 절충안이 있는 경우가 많습니다. 동기화 전략을 구현할 때 성능을 위해 맹목적으로 단순성을 희생해서는 안 됩니다(이로 인해 보안이 손상될 수 있음). //보안과 성능 사이의 균형을 이루기 위해 노력합니다.

        계산 집약적인 작업을 수행하든 잠재적으로 차단하는 작업을 수행하든 관계없이 잠금이 너무 오랫동안 유지되면 활성 또는 성능 문제가 발생합니다. 따라서 긴 계산이나 빠르게 완료되지 않는 작업(예: 네트워크 I/O 또는 콘솔 I/O)을 수행할 때는 잠금을 유지해서는 안 됩니다 . // 오랫동안 실행되는 코드는 잠금을 유지해서는 안 됩니다.

        이 시점에서 전체 텍스트는 여기서 끝납니다.

추천

출처blog.csdn.net/swadian2008/article/details/125164826