고급 동기화 - concurrent.Lock

황상익·2024년 10월 10일

Inflearn JAVA

목록 보기
49/61

LockSupport 1

synchronized 단점

  • 무한대기
  • 공정성

LockSupport 기능

LockSupport는 스레드를 waiting 상태로 변경
Waiting 상태는 누가 깨워주기 전까지는 계속 대기, CPU 스케줄링에 포함되지는 않는다.

  • park : 스레드를 waiting 상태로 변경
  • parkNanos : 나노초 동안만 TIMED_WAITING 상태로 변경
    지정한 나노초가 지나면, Runnable 상태로 변경
  • unpark(thread) : waiting 상태의 대상 스레드를 Runnable 상태로 변경

LockSupport 기능

public class LockSupportMainV1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ParkTask(), "T1");
        t1.start();

        //잠시 대기하여 t1이 Park 상태에 빠질 시간을 준다.
        sleep(100);
        log("T1 state : " + t1.getState());

        log("main -> unpark(T1)"); //unpark 사용해서 t1을 꺠움
        //LockSupport.unpark(t1); //unpark 사용
        t1.interrupt(); //interrupt 사용 waiting 상태를 빠져나온다.
    }

    static class ParkTask implements Runnable {

        @Override
        public void run() {
            log("Park 시작");
            LockSupport.park(); //t1은 이 코드를 실행할때 sleep을 함 -> main Thread가 대기
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태 : " + Thread.currentThread().isInterrupted());
        }
    }
}


main tmfpemrk t1.start 호출 -> Runnable
t1 -> park 호출 -> Runnable -> Waiting 상태
t1을 unpark 상태로 깨운다.

대기 상태로 바꾸는 LockSupprot.park는 매개변수가 없는데, 실행 가능 상태로 변경하는 unpark는 왜 특정 스레드를 지정하는 매개변수가 없을까??
-> park를 호출해서 스스로 대기 상태에 들어갈 수는 있지만, 대기상태의 코드는 자신을 실행 할 수 없기떄문

인터럽트 사용

Waiting 상태의 스레드에 인터럽트 발생, Waiting -> Runnable

//LockSupport.unpark(thread1); //1. unpark 사용
thread1.interrupt(); //2. interrupt() 사용

LockSupprot 2

시간 대기
특정 시간 동안만 대기하는 parkNanos(nanos)를 호출
스레드를 나노초 동안만 TIMED_WAITING 상태로 변경, 지정한 나노초가 지나면, TIMED_WAITING 상태에서 빠져나와 Runnable

public class LockSupportMainV2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ParkTask(), "T1");
        t1.start();

        //잠시 대기하여 t1이 Park 상태에 빠질 시간을 준다.
        sleep(100);
        log("T1 state : " + t1.getState());
    }

    static class ParkTask implements Runnable {

        @Override
        public void run() {
            log("Park 시작, 2초 대기");
            LockSupport.parkNanos(2000_000000); //2초 뒤 깨어난다.
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태 : " + Thread.currentThread().isInterrupted());
        }
    }
}

Blocked Vs Waiting

Waiting 상태에 특정 시간 까지만 대기하는 기능이 포함된 것이 TIMED_WAITING

  • 인터럽트
    BLOCKED 상태는 인터럽트가 걸려도 대기 상태를 빠져나오지 못한다.
    WAITING, TIMED_WAITING 상태는 인터럽트가 걸리면 대기 상태에서 나온다.

Blocked는 자바의 synchornized에서 락을 획득하기 위해 대기 할때 사용
WAITING, TIMED_WAITING은 스레드가 특정 조건이나 시간동안 대기할때 발생
WAITING 상태는 다양한 상황에서 사용, Thread.join(), LockSupport.park(),
Object.wait()과 같은 메서드 호출시 WAITING

대기상태와 시간 대기 상태는 서로 짝이 없다.

Thread.join(), Thread.join(long millis)
Thread.park(), Thread.parkNanos(long millis)
Object.wait(), Object.wait(long timeout)

LockSupport 정리

lock 이라는 클래스를 생성, 특정 스레드가 먼저 락을 획득, Runnable로 실행, 락을 획득 X, park를 사용해서 대기 상태로 만듬, 스레드가 임계영역의 실행을 마치고나면 락을 반납
unpark를 사용하면 대기중인 다른 스레드를 깨움.

LockSupport의 경우 너무 저수준.

ReentrantLock

lock 인터페이스

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
  1. void lock
    락을 획득, 다른 스레드 이미 락 획득, 락이 풀릴때까지 현재 스레드는 대기, 이 메서드는 인터럽트에 반응X

  2. void lockInterruptibly
    락 획든은 시도, 다른 스레드가 인터럽트 할 수 있도록. 다른 스레드가 이미 락을 획득, 현재 스레드는 대기 큐에서 락을 획득할 때까지 대기. 대기 중에 인터럽트 발생 -> InterruptException 발생

  3. boolean trylock()
    락 획득을 시도, 즉시 성공여부 반환, 다른 스레드가 락을 획득 -> false, 락을 획득하면 true

  4. boolean trylock(long time, TimeUnit unit)
    주어진 시간동안 락 획득 시도, 주어진 시간 안에 락을 획득, true, 획득하지 못한면 false

  5. void unlock()
    락을 해제, 락을 해제하면 락 획득을 대기중인 스레드 중 하나가 락을 획득
    락을 획득한 스레드가 호출, 그렇지 않으면 IllegalMonitorStateException 발생

  6. Conditon newCondition()
    Conditon 객체를 생성하여 반환. Conditon 객체는 락과 결합, 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록

public class ReentrantLockEx {
// 비공정 모드 락
private final Lock nonFairLock = new ReentrantLock();
// 공정 모드 락
private final Lock fairLock = new ReentrantLock(true);
public void nonFairLockTest() {
nonFairLock.lock();
try {
// 임계 영역
} finally {
nonFairLock.unlock();
}
}
public void fairLockTest() {
fairLock.lock();
try {
// 임계 영역
} finally {
fairLock.unlock();
}
}
}

비공정 모드

먼저 락을 요청한 스레드가 락을 먼저 획득, 락을 풀었을때 아무나 획득가능, 특정 스레드가 장시간 락을 획득 못할 가능성이 있음

성능 우선: 락을 획득하는 속도가 빠르다.
선점 가능: 새로운 스레드가 기존 대기 스레드보다 먼저 락을 획득할 수 있다.
기아 현상 가능성: 특정 스레드가 계속해서 락을 획득하지 못할 수 있다.

공정모드

생성자에서 true를 전달
공정모드는 락을 요청한 순서대로, 락을 획득 가능. 먼저 대기한 스레드가 먼저 락을 획득
스레드의 공정성 보장. 단 성능 ISSUE

public class BankAccountV4 implements BankAccount {

    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccountV4(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        lock.lock(); //ReentrantLock 이용하여 Lock 걸기

        //lock을 걸고 나면 무조건 try ~ finally를 걸어서 무조건 unlock이 되게 한다.
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000);
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
            //== 임계 영역 ==
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
            //lock을 걸면 무조건 unLock을 해줘야 함 -> waiting 걸린 애가 빠져나오지 못함
        }

        log("거래 종료");
        return true;
    }

    @Override
    public synchronized int getBalance() {
        lock.lock();//ReentrantLock 이용하여 Lock 걸기
        try {
            return balance;
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
        }
    }
}

여기서 사용하는 lock 객체는 내부에 있는 모니터 락이 아니다. lock 인터페이스와 ReentrantLock이 제공하는 기능
모니터 락과 BLOCKED 상태는 synchronized에서만 사용

ReentrantLock 대기 중단

tryLock()

public class BankAccountV5 implements BankAccount {

    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccountV5(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        // lock을 얻을 수 있으면 얻고, 얻을 수 없다면, 바로 반환
        if (!lock.tryLock()) {
            log("[진입 실패] : 이미 처리중인 작업이 있습니다.");
            return false;
        }

        //lock을 걸고 나면 무조건 try ~ finally를 걸어서 무조건 unlock이 되게 한다.
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000);
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
            //== 임계 영역 ==
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
            //lock을 걸면 무조건 unLock을 해줘야 함 -> waiting 걸린 애가 빠져나오지 못함
        }

        log("거래 종료");
        return true;
    }

    @Override
    public synchronized int getBalance() {
        lock.lock();//ReentrantLock 이용하여 Lock 걸기
        try {
            return balance;
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
        }
    }
}

tryLock(시간)

public class BankAccountV6 implements BankAccount {

    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccountV6(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());


        try {
            // lock을 얻을 수 있으면 얻고, 얻을 수 없다면, 바로 반환
            if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                log("[진입 실패] : 이미 처리중인 작업이 있습니다.");
                return false;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //lock을 걸고 나면 무조건 try ~ finally를 걸어서 무조건 unlock이 되게 한다.
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000);
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
            //== 임계 영역 ==
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
            //lock을 걸면 무조건 unLock을 해줘야 함 -> waiting 걸린 애가 빠져나오지 못함
        }

        log("거래 종료");
        return true;
    }

    @Override
    public synchronized int getBalance() {
        lock.lock();//ReentrantLock 이용하여 Lock 걸기
        try {
            return balance;
        } finally {
            lock.unlock();// ReentrantLock -> lock 해제
        }
    }
}
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글