Lock 및 AbstractQueuedSynchronizer(AQS) 초기 인식

11007 단어 Java 동시 실행

1. concurrent 패키지의 구조적 차원


동시 프로그래밍에 있어 Doug Lea 마스터는 우리에게 실용적이고 고성능의 도구류를 대량으로 제공했다. 이런 코드에 대한 연구를 하면 우리 팀의 동시 프로그래밍에 대한 파악이 더욱 투철해지고 우리 팀의 동시 프로그래밍 기술에 대한 애착도 크게 높아질 것이다.이 코드들은java에 있습니다.util.concurrent 패키지.다음 그림은 concurrent 패키지의 디렉터리 구조도입니다.
그중에는 두 개의 하위 패키지가 포함되어 있는데 그것이 바로 아토믹과 lock이다. 또한 concurrent 아래의 막힌 대기열과executors이다. 이것들이 바로 concurrent 패키지의 정수이다. 이후에 하나하나 학습할 것이다.이러한 유형의 실현은 주로volatile과CAS(volatile에 관해서는 이 글을 볼 수 있고 CAS에 관해서는 이 글을 볼 수 있는 3.1절)에 의존한다. 전체적으로 보면concurrent 패키지의 전체적인 실현도는 다음과 같다.

2. lock 소개


우리 내려와서 콘커런트 가방 밑에 있는 lock 가방을 보자.자물쇠는 여러 라인이 공유 자원에 접근하는 것을 제어하는 방식이다. 일반적으로 하나의 자물쇠는 여러 라인이 공유 자원에 동시에 접근하는 것을 방지할 수 있다.Lock 인터페이스가 등장하기 전에 자바 프로그램은 주로synchronized 키워드로 잠금 기능을 실현했고 자바 SE5 이후 하청에 lock 인터페이스를 추가해서synchronized와 같은 잠금 기능을 제공했다.synchronize 키워드와 같은 은밀한 잠금 해제의 편리성을 잃었지만 잠금 획득과 방출의 조작성, 중단 가능한 잠금 획득, 시간 초과 잠금 등 여러 synchronized 키워드가 갖추지 못한 동기화 특성을 가지고 있다.일반적으로 표시를 사용하여 다음과 같은 lock을 사용합니다.
Lock lock = new ReentrantLock();
lock.lock();
try{
    .......
}finally{
    lock.unlock();
}

주의해야 할 것은synchronized 동기화 블록이 실행되거나 이상이 발생하면 자물쇠가 자동으로 방출되며, lock는 unlock () 방법을 사용해서 자물쇠를 방출해야 하기 때문에finally 블록에서 자물쇠를 방출해야 합니다.

2.1 Lock 인터페이스 API


이제 lock 인터페이스가 정의하는 방법을 살펴보겠습니다.
void lock();//잠금void lockInterruptibly () throws InterruptedException 가져오기//잠금을 가져오는 동안 중단된 boolean tryLock () 에 응답할 수 있습니다.//비차단식 응답 중단은 즉시 반환되며, 잠금을 가져와true로 되돌려주고fasle boolean try Lock(long time, Time Unit unit) throws Interrupted Exception으로 되돌려줍니다.시간 초과 잠금을 가져옵니다. 시간 초과 내나 중단되지 않은 상태에서 잠금 컨디션 new Condition () 을 가져옵니다.//lock와 연결된 대기 알림 구성 요소를 가져옵니다. 현재 라인은 자물쇠를 가져와야 대기할 수 있습니다. 대기할 때 자물쇠를 먼저 풀고, 다시 자물쇠를 가져올 때 대기에서 되돌아올 수 있습니다.
위에는 lock 인터페이스 아래의 다섯 가지 방법이 있는데 원본 코드에서 영역을 한 번 번역했을 뿐이고 관심 있는 것은 스스로 가서 볼 수 있다.그러면locks 패키지 아래에서 어떤 종류가 이 인터페이스를 실현했습니까?가장 익숙한 Reentrant Lock부터 말씀드리겠습니다.
public class ReentrantLock implements Lock, java.io.Serializable
Reentrant Lock이 lock 인터페이스를 실현한 것은 분명하다. 다음에 어떻게 실현되었는지 자세히 연구해 보자.원본 코드를 볼 때 Reentrant Lock에 코드가 별로 없다는 것을 놀라게 할 것이다. 또 하나의 뚜렷한 특징은 기본적으로 모든 방법의 실현은 실제적으로 정적 메모리 클래스Sync의 방법을 호출했고 Sync 클래스는 계승AbstractQueuedSynchronizer(AQS)되었다는 것이다.ReentrantLock을 이해하려면 대기열 동기화기AbstractQueuedSynchronizer(약칭 동기화)에 대한 이해가 관건이라는 것을 알 수 있다.

2.2 AQS 초심자


소스에서 AQS에 대한 구체적인 설명은 다음과 같습니다.
 Provides a framework for implementing blocking locks and related
 synchronizers (semaphores, events, etc) that rely on
 first-in-first-out (FIFO) wait queues.  This class is designed to
 be a useful basis for most kinds of synchronizers that rely on a
 single atomic {@code int} value to represent state. Subclasses
 must define the protected methods that change this state, and which
 define what that state means in terms of this object being acquired
 or released.  Given these, the other methods in this class carry
 out all queuing and blocking mechanics. Subclasses can maintain
 other state fields, but only the atomically updated {@code int}
 value manipulated using methods {@link #getState}, {@link
 #setState} and {@link #compareAndSetState} is tracked with respect
 to synchronization.

 

Subclasses should be defined as non-public internal helper classes that are used to implement the synchronization properties of their enclosing class. Class {@code AbstractQueuedSynchronizer} does not implement any synchronization interface. Instead it defines methods such as {@link #acquireInterruptibly} that can be invoked as appropriate by concrete locks and related synchronizers to implement their public methods.


동기화기는 자물쇠와 다른 동기화 구성 요소를 구축하는 기초 구조로, 동기화 상태를 표시하고, FIFO 대기열을 통해 대기 대기열을 구성하는 int 구성원 변수에 의존한다.그것의 하위 클래스는 AQS의 몇 가지 보호된 수식을 다시 써서 동기화 상태를 바꾸는 방법을 사용해야 한다. 다른 방법은 주로 줄 서기와 차단 메커니즘을 실현했다.상태 업데이트는 getState, setState,compareAndSetState 세 가지 방법을 사용합니다.
하위 클래스는 사용자 정의 동기화 구성 요소의 정적 내부 클래스로 정의됩니다. 동기화 자체는 동기화 인터페이스를 실현하지 못했습니다. 이것은 단지 몇 가지 동기화 상태의 획득과 방출 방법을 정의하여 사용자 정의 동기화 구성 요소의 사용을 제공할 뿐입니다. 동기화 장치는 단독 동기화 상태도 지원하고 공유 동기화 상태도 지원합니다. 이렇게 하면 서로 다른 유형의 동기화 구성 요소를 쉽게 실현할 수 있습니다.
싱크로나이즈드는 자물쇠(임의의 싱크로나이즈드 구성 요소)를 실현하는 관건이다. 자물쇠의 실현에 있어 싱크로나이즈드를 모으고 싱크로나이즈드를 이용하여 자물쇠의 의미를 실현한다.이렇게 양자의 관계를 이해할 수 있다. 자물쇠는 사용자를 대상으로 하는 것으로 사용자와 자물쇠가 상호작용하는 인터페이스를 정의하고 세부 사항을 숨긴다.동기화기는 자물쇠의 실현자를 대상으로 자물쇠의 실현 방식을 간소화하고 동기화 상태의 관리, 라인의 줄 서기, 대기와 깨우기 등 밑바닥 조작을 차단한다.자물쇠와 동기화기는 사용자와 실현자가 주목해야 할 분야를 잘 격리시켰다.

2.3 AQS의 템플릿 메소드 디자인 모델


AQS의 디자인은 템플릿 방법의 디자인 모델로 일부 방법을 하위 클래스에 개방하여 다시 쓰기를 하고, 동기화기가 동기화 구성 요소에 제공하는 템플릿 방법은 이불 클래스가 다시 쓰기를 하는 방법을 호출한다.예를 들어 AQS에서 다시 쓰는 방법tryAcquire:
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

ReentrantLock에서 NonfairSync(AQS 상속)는 다음 메서드를 다시 씁니다.
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

AQS의 템플릿 메서드 acquire():
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }

tryAcquire 방법이 호출되고, AQS의 NonfairSync 호출 템플릿 방법인 acquire를 계승할 때, NonfairSync가 다시 쓴 tryAcquire 방법이 호출됩니다.이것이 바로 AQS를 사용하는 방식이다. 이 점을 알게 되면 lock의 실현과 이해가 크게 향상된다.요약하면 다음과 같습니다.
  • 동기화 구성 요소(이곳은 값 잠금뿐만 아니라 CountDownLatch 등도 포함)의 실현은 동기화기 AQS에 의존한다. 동기화 구성 요소의 실현에서 AQS를 사용하는 방식으로 AQS를 계승하는 정적 메모리 클래스를 정의하는 것을 추천한다.
  • AQS는 템플릿 방법으로 디자인을 하고 AQS의 보호된 수식 방법은 AQS의 하위 클래스를 계승하여 다시 써야 하며 AQS의 하위 클래스를 호출할 때 다시 쓰는 방법을 호출한다.
  • AQS는 동기화 상태의 관리, 라인의 줄 서기, 대기 및 깨우기 등 하위 조작을 담당하고 Lock 등 동기화 구성 요소는 주로 동기화 의미를 실현하는 데 전념한다.
  • AQS를 다시 쓸 때 AQS에서 제공하는 getState(),setState(),compareAndSetState() 방법으로 동기화 상태 수정
  • AQS에서 다시 쓸 수 있는 방법은 다음과 같다(에서 발췌한 것).
    구성 요소 동기화를 수행할 때 AQS에서 제공하는 템플릿 방법은 다음과 같습니다.
    AQS에서 제공하는 템플릿 방법은 3가지 유형으로 나눌 수 있습니다. 1.독점식 획득 및 방출 동기화 상태;2. 공유식 획득과 방출의 동기화 상태;3. 동기화 대기열에서 대기 라인의 상황을 조회한다.
    동기화 구성 요소는 AQS에서 제공하는 템플릿 방법을 통해 동기화 의미를 구현합니다.

    3. 하나의 예


    다음은 AQS의 사용을 한층 더 이해하기 위해 하나의 예를 사용한다.이 예도 AQS 원본의 example에서 유래한 것이다.
    class Mutex implements Lock, java.io.Serializable {
        // Our internal helper class
        //   AQS      
        //     
        private static class Sync extends AbstractQueuedSynchronizer {
            // Reports whether in locked state
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            // Acquires the lock if state is zero
            public boolean tryAcquire(int acquires) {
                assert acquires == 1; // Otherwise unused
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            // Releases the lock by setting state to zero
            protected boolean tryRelease(int releases) {
                assert releases == 1; // Otherwise unused
                if (getState() == 0) throw new IllegalMonitorStateException();
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            // Provides a Condition
            Condition newCondition() {
                return new ConditionObject();
            }
    
            // Deserializes properly
            private void readObject(ObjectInputStream s)
                    throws IOException, ClassNotFoundException {
                s.defaultReadObject();
                setState(0); // reset to unlocked state
            }
        }
    
        // The sync object does all the hard work. We just forward to it.
        private final Sync sync = new Sync();
        //                   
        public void lock() {
            sync.acquire(1);
        }
    
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        public void unlock() {
            sync.release(1);
        }
    
        public Condition newCondition() {
            return sync.newCondition();
        }
    
        public boolean isLocked() {
            return sync.isHeldExclusively();
        }
    
        public boolean hasQueuedThreads() {
            return sync.hasQueuedThreads();
        }
    
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
    }
    

    MutexDemo:
    public class MutextDemo {
        private static Mutex mutex = new Mutex();
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(() -> {
                    mutex.lock();
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        mutex.unlock();
                    }
                });
                thread.start();
            }
        }
    }
    

    실행:
    위의 이 예는 독점 자물쇠의 의미를 실현했고 같은 시간에 한 라인만 자물쇠를 점유할 수 있다.Mutex Demo는 각각 3s 수면을 취하는 10개의 스레드를 새로 만들었습니다.실행 상황을 보면 현재 Thread-6이 자물쇠를 차지하고 있고 다른 Thread-7, Thread-8 등 라인은 WAIT 상태에 있음을 알 수 있다.추천하는 방식에 따라 Mutex는 AQS를 계승하는 정적 내부 클래스인 Sync를 정의하고 AQS의tryAcquire를 다시 쓰는 등 방법을 정의했다.state에 대한 업데이트도 setState(), getState(), 컴퓨터AndSetState(), 이 세 가지 방법을 이용했다.lock 인터페이스를 실현하는 방법도 AQS가 제공하는 템플릿 방법만 호출했습니다. (Sync가 AQS를 계승하기 때문입니다.)이 예에서 알 수 있듯이 동기화 구성 요소의 실현은 주로 AQS를 이용했고 AQS는 동기화 상태의 수정을 차단했고 라인 줄 서기 등 밑바닥에서 이루어졌다. AQS의 템플릿 방법을 통해 동기화 구성 요소의 실현자에게 쉽게 호출할 수 있다.사용자에게 동기화 구성 요소가 제공하는 방법을 호출하여 병렬 프로그래밍을 실현하기만 하면 된다.동시에 새 동기화 구성 요소를 만들 때 파악해야 할 두 가지 관건은 다음과 같다.동기화 구성 요소를 실현할 때 AQS를 계승하는 정적 메모리 클래스를 정의하고 필요한 보호된 수식 방법을 다시 쓰는 것을 추천합니다.2. 동기화 구성 요소의 의미의 실현은 AQS의 템플릿 방법에 의존하고 AQS 템플릿 방법은 AQS의 하위 클래스에 의해 다시 쓰는 방법에 의존한다.
    통속적으로 말하면 AQS의 전체적인 디자인 사고방식은 템플릿 방법으로 디자인 모델을 사용하기 때문에 동기화 구성 요소와 AQS의 기능은 실제적으로 각자의 두 부분으로 나누어지지 않는다.
    동기화 어셈블리 구현자 각도:
    다시 쓸 수 있는 방법: 독점식:tryAcquire () (독점식 동기화 상태 가져오기),tryRelease () (독점식 동기화 상태 방출);공유식:tryAcquireshared()(공유식 동기화 상태 가져오기),tryReleaseshared()(공유식 동기화 상태 해제);AQS에서 현재 동기화 상태가 성공적으로 검색되었는지 또는 성공적으로 해제되었는지 판단하는 방법을 알려줍니다.동기화 구성 요소는 현재 동기화 상태에 대한 논리적 판단에 집중하여 자신의 동기화 의미를 실현한다.이 말은 비교적 추상적이다. 예를 들어 위의 Mutex 예에서tryAcquire 방법을 통해 자신의 동기화 의미를 실현한다. 이 방법에서 현재 동기화 상태가 0 (즉 이 동기화 구성 요소가 어떤 루트도 가져오지 않았음) 이면 현재 루트는 가져오는 동시에 상태를true로 되돌려줍니다. 그렇지 않으면 이 구성 요소는 루트가false로 되돌아옵니다.분명히 이 동기화 구성 요소는 같은 시간에만 라인에 점용될 수 있으며, Mutex는 자신이 표현하고자 하는 동기화 의미를 실현하기 위해 방출된 논리를 가져오는 데 전념하고 있다.
    AQS 각도
    AQS에 대해 말하자면 동기화 구성 요소가 되돌아오는true와false만 있으면 된다. AQS는true와false에 대해 서로 다른 조작을 할 수 있기 때문에true는 현재 스레드가 동기화 구성 요소를 가져오는 데 성공했다고 생각하고false는AQS도 현재 스레드를 동기화 대기열에 삽입하는 등 일련의 방법을 사용한다.
    전체적으로 말하면 동기화 구성 요소는 AQS를 다시 쓰는 방법을 통해 자신이 표현하고자 하는 동기화 의미를 실현하고 AQS는 동기화 구성 요소가 표현하는true와false만 있으면 된다. AQS는true와false의 서로 다른 상황에 대해 서로 다른 처리를 하고 밑바닥에서 실현하면 이 글을 볼 수 있다.
    참고 문헌
    《java 병렬 프로그래밍의 예술》

    좋은 웹페이지 즐겨찾기