J-C-06 멀티스레드에서 큐가 깨지는 이유와 CAS로 Lock free 구조까지 가는 길

글 정보
카테고리
Programming/Java/Core
태그
JavaLevel2

멀티스레딩에서 터지는 곳은 보통 자료구조입니다

큐가 왜 자주 등장하나

연결 리스트와 멀티스레드를 같이 쓰면 뭐가 깨지나

레이스 컨디션이 만들어지는 전형적 상황

임계 영역은 어디로 잡아야 하나

예제 코드로 보는 레이스 포인트

노드 추가 appendNode

public boolean appendNode(String name) {
    UserData newUser = new UserData(name);
    newUser.prev = tail.prev;
    newUser.next = tail;
    tail.prev.next = newUser; // A Start
    tail.prev = newUser;
    ++counter; // A End
    return true;
}

counter도 같이 깨집니다

노드 삭제 removeAtHead

public UserData removeAtHead() {
    if (isEmpty()) return null;
    UserData node = head.next; // 임계영역 Start
    node.next.prev = head;
    head.next = node.next;
    --counter; // 임계영역 End
    return node;
}

join이 있을 때는 왜 멀쩡해 보이나

join 없이 동시에 돌리면 왜 터지나

해결 1 synchronized로 감싸기

public synchronized boolean appendNode(String name) { ... }
public synchronized UserData removeAtHead() { ... }

해결 2 ReentrantLock으로 임계 영역만 줄이기

import java.util.concurrent.locks.ReentrantLock;
class LinkedQueue {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;
    public boolean appendNode(String name) {
        lock.lock();
        try {
            // 링크 갱신 + counter 갱신만 보호
            // tail.prev.next = newUser;
            // tail.prev = newUser;
            // counter++;
            return true;
        } finally {
            lock.unlock();
        }
    }
    public UserData removeAtHead() {
        lock.lock();
        try {
            // 링크 갱신 + counter 갱신만 보호
            // head.next = node.next;
            // node.next.prev = head;
            // counter--;
            return null;
        } finally {
            lock.unlock();
        }
    }
}

CAS Compare And Swap이란 무엇인가

왜 CAS가 Lock free를 가능하게 하나

Spin lock은 무엇인가

CAS 기반 SpinLock 예제

AtomicBoolean + LockSupport.parkNanos 예제

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
class SpinLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 너무 공격적으로 돌면 CPU를 태움
            LockSupport.parkNanos(1_000); // 1us 정도 쉬며 재시도
        }
    }
    public void unlock() {
        locked.set(false);
    }
}

Lock free 큐가 좋은 예가 되는 이유

AtomicReference를 쓰는 이유

lock free가 항상 이기나

결론