Java adv1 - LockSupport3

dev1·2024년 11월 29일

그럼 이제, 본격적으로 LockSupport 가 어떻게 synchronized 의 무한대기를 해결할 수 있는지 확인해보자.

결국 무한대기가 되는이유가 ... Block 상태여서 그런것인데.. ( 락을 대기하는 )

즉, 특정시간까지만 기다려보고 아니면 그냥 다음번에 시도하라던가, 다른방법으로 시도하라던가 등등...

이러한 방식으로 유도하면 되지않을까?

즉, 특정 스레드가 락을 얻어서 실행 ==> 나머지 다른 스레드들은 그냥 무한정 대기하는게아니라,

LockSupport.park 를 사용해서 대기상태로 두는것이다.

==> parkNanos 를 주로 사용할듯 ? ( 특정 시간만 대기하고 그래도 실행이 안됐으면 .. => 다른... )

그럼 어떻게 이러한 기능을 구현할 수 있을까?

====> 자바에서 제공하는 기능을 사용하자. ( Lock, ReentrantLock )

그럼 이제, 이러한 기능들을 어떻게 사용하는지 알아보자.

[[ 참고로, interrupt 는 Block 상태를 깨우지 못한다 ]]

Lock => 무한대기 상태를 해결
ReentrantLock => 공정 문제 해결 ( 먼저 기다린 스레드가 락을 획득할 수 있도록 설정 ).

이를 활용하기위해, 이전에 있었던 출금기능 예제를 가져와보자.

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV3 implements BankAccount {

    private int balance;

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

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

        synchronized (this) {
            log("[검증을 시작합니다] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            if ( balance < amount ) {
                log("[검증 실패] 출금액 : " + amount + " 보유중인 금액 : " + balance + ", 잔액 부족입니다.");
                return false;
            }

            // 출금가능상태
            log("[검증 완료] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            log("[처리중]");
            sleep(1000);
            balance -= amount;
            log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);
        }

        log("거래를 종료합니다.");
        return true;
    }

    @Override
    public synchronized int getBalance() {
        return balance;
    }

}

==> synchronized 키워드를 사용하니 ... synchronized 단점이 발생 ( 무한대기, 공정성 )

이제, 해결해보자.

package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

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 걸어버림 // 한번에 하나의 스레드만 접근할 수 있게

        try {
            log("[검증을 시작합니다] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            if ( balance < amount ) {
                log("[검증 실패] 출금액 : " + amount + " 보유중인 금액 : " + balance + ", 잔액 부족입니다.");
                return false;
            }
            // 출금가능상태
            log("[검증 완료] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            log("[처리중]");
            sleep(1000);
            balance -= amount;
            log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);
        }
        finally {
            lock.unlock(); // 무조건 unlock 필요 ==> try 안에 있는거에서 문제생겨도 일단 그냥 unlock ==> unlock : lock 해제
        }

        log("거래를 종료합니다.");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        }
        finally {
            lock.unlock();
        }
    }

}

락을만들고, 임계영역이 필요한부분에 lock, unlock 으로 감싸주면 된다.

락을 만듦 => private final Lock lock = new ReentrantLock();

추가로, try / finally 를 왜 씀?

===> try 쪽에 있는 코드부분에서 리턴이 나타나거나 / 예외가 발생하게되면 밑에있는 코드 실행 안함

이렇게되면, lock.lock() 을 통해서 lock 상태로 걸어놨는데 이를 풀지를못함.

그래서, 임계영역 코드 다 진행하고나서, 풀어줄 수 있는 lock.unlock() 을 반드시 실행

그래서 try{} ==> try 안에서 문제가 생기든말든, 뭐가됐든간에 반 드 시 => finally 에 있는거 실행 ( unlock )

package thread.sync.test;

import thread.sync.BankAccount;
import thread.sync.BankAccountV3;
import thread.sync.BankAccountV4;
import thread.sync.WithdrawTask;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankMain4 {

    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccountV4(1000);

        Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
        Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");

        t1.start();
        t2.start();

        sleep(500);
        log("t1 state : " + t1.getState());
        log("t2 state : " + t2.getState());

        t1.join();
        t2.join();

        log("최종 잔액 : " + account.getBalance());
    }
}

===> 이제 코드를 돌려보자.

/Users/hoon/Library/Java/JavaVirtualMachines/openjdk-23.0.1/Contents/Home/bin/java -javaagent:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=49690:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/hoon/Desktop/java/java-adv1/out/production/java-adv1 thread.sync.test.BankMain4
17:29:04.639 [       t1] 거래 시작 : BankAccountV4
17:29:04.639 [       t2] 거래 시작 : BankAccountV4
17:29:04.642 [       t1] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 1000
17:29:04.642 [       t1] [검증 완료] 출금액 : 800 보유중인 금액 : 1000
17:29:04.642 [       t1] [처리중]
17:29:05.132 [     main] t1 state : TIMED_WAITING
17:29:05.132 [     main] t2 state : WAITING
17:29:05.648 [       t1] [검증 완료] 출금액 : 800 출금 후 잔액 : 200
17:29:05.649 [       t1] 거래를 종료합니다.
17:29:05.649 [       t2] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 200
17:29:05.652 [       t2] [검증 실패] 출금액 : 800 보유중인 금액 : 200, 잔액 부족입니다.
17:29:05.656 [     main] 최종 잔액 : 200

Process finished with exit code 0

원하는 로직기능 잘 수행됨.

lock 을 이용해서 간단하게 synchronized 에 있었던 문제를 해결하면서, 동시성 문제도 같이 해결

이제, 하나만 더 알아보자.

==> 무한대기 하고 있는 상태를 해결했지만... 잘 생각해보면,

대기중인 스레드가 매우매우 많다면( 셀수없을정도로 ). => 가장 마지막에 실행될 스레드는 너무 오래 걸릴 수 있음

그래서, 특정시간만 대기해보고, 이후에 다른방향으로 이끌어주면 ... ( 재시도유도, 다른방법시도를 유도하거나 ).

===> 즉, 특정시간동안만 락 획득을 위해 대기하게 할 수 있음

===> tryLock()

package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

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.lock(); // ==> ReentrantLock 을 통해서 lock 걸어버림 // 한번에 하나의 스레드만 접근할 수 있게

        try {
            if ( lock.tryLock() ){

                log("[검증을 시작합니다] 출금액 : " + amount + " 보유중인 금액 : " + balance);
                if ( balance < amount ) {
                    log("[검증 실패] 출금액 : " + amount + " 보유중인 금액 : " + balance + ", 잔액 부족입니다.");
                    return false;
                }
                // 출금가능상태
                log("[검증 완료] 출금액 : " + amount + " 보유중인 금액 : " + balance);
                log("[처리중]");
                sleep(1000);
                balance -= amount;
                log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);

            }
            System.out.println("다시 시도해주세요.");
        }
        finally {
            lock.unlock(); // 무조건 unlock 필요 ==> try 안에 있는거에서 문제생겨도 일단 그냥 unlock ==> unlock : lock 해제
        }

        log("거래를 종료합니다.");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        }
        finally {
            lock.unlock();
        }
    }

}

우선은, 파라미터를 넘겨주지 않는 tryLock() 를 확인해보자.

===> lock.tryLock() :::: boolean 형태로 반환값을 ...

즉, 락 획득을 시도 ... => 바로 획득했다면 : true

반면에, 락 획득을 시도했음 => 바로 획득하지못했다면 : false

위에 있는 코드를 통해서 확인해보면 ....

시도 => 바로 획득 :: 검증로직 실행

시도 => 바로 획득 못하면 :: 다시 시도하라고 출력

위에있는 코드를 통해서 객체를 생성하고 확인해보면 ....

/Users/hoon/Library/Java/JavaVirtualMachines/openjdk-23.0.1/Contents/Home/bin/java -javaagent:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=49708:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/hoon/Desktop/java/java-adv1/out/production/java-adv1 thread.sync.test.BankMain4
17:36:18.694 [       t1] 거래 시작 : BankAccountV5
17:36:18.694 [       t2] 거래 시작 : BankAccountV5
17:36:18.698 [       t1] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 1000
17:36:18.698 [       t1] [검증 완료] 출금액 : 800 보유중인 금액 : 1000
17:36:18.698 [       t1] [처리중]
17:36:19.185 [     main] t1 state : TIMED_WAITING
17:36:19.186 [     main] t2 state : WAITING
17:36:19.704 [       t1] [검증 완료] 출금액 : 800 출금 후 잔액 : 200
다시 시도해주세요.
17:36:19.705 [       t1] 거래를 종료합니다.

이렇게 나타나게됨.

t1 이 실행하고있고, 끝나기전에 t2 가 획득시도 ==> t1 이 아직 실행이니까 락을 획득하는데에 실패 ..

==> 다시 시도해주세요. 라고 메세지 출력

좀더 명확히 확인하기위해, 코드를 수정해보자.

다시 시도해주세요. 를 출력하는 코드를 => log("다시 시도해주새요.");로 변경

/Users/hoon/Library/Java/JavaVirtualMachines/openjdk-23.0.1/Contents/Home/bin/java -javaagent:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=49711:/Users/hoon/Desktop/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/hoon/Desktop/java/java-adv1/out/production/java-adv1 thread.sync.test.BankMain4
17:37:53.743 [       t2] 거래 시작 : BankAccountV5
17:37:53.743 [       t1] 거래 시작 : BankAccountV5
17:37:53.748 [       t2] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 1000
17:37:53.748 [       t2] [검증 완료] 출금액 : 800 보유중인 금액 : 1000
17:37:53.748 [       t2] [처리중]
17:37:54.235 [     main] t1 state : WAITING
17:37:54.236 [     main] t2 state : TIMED_WAITING
17:37:54.754 [       t2] [검증 완료] 출금액 : 800 출금 후 잔액 : 200
17:37:54.754 [       t2] 다시 시도해주새요.
17:37:54.755 [       t2] 거래를 종료합니다.

그럼 이제, 파라미터를 받는 tryLock() 을 확인해보자.

package thread.sync;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

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());
        lock.lock(); // ==> ReentrantLock 을 통해서 lock 걸어버림 // 한번에 하나의 스레드만 접근할 수 있게

        try {
            if (lock.tryLock(500, TimeUnit.MILLISECONDS)){
                log("다시 시도해주세요.");
                return false;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
                log("[검증을 시작합니다] 출금액 : " + amount + " 보유중인 금액 : " + balance);
                if ( balance < amount ) {
                    log("[검증 실패] 출금액 : " + amount + " 보유중인 금액 : " + balance + ", 잔액 부족입니다.");
                    return false;
                }
                // 출금가능상태
                log("[검증 완료] 출금액 : " + amount + " 보유중인 금액 : " + balance);
                log("[처리중]");
                sleep(1000);
                balance -= amount;
                log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);

        }
        finally {
            lock.unlock(); // 무조건 unlock 필요 ==> try 안에 있는거에서 문제생겨도 일단 그냥 unlock ==> unlock : lock 해제
        }

        log("거래를 종료합니다.");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        }
        finally {
            lock.unlock();
        }
    }

}

얼마나 기다릴지를 넣어주고, 시간의 단위는 어떻게 할지 정해주면 됨.

0개의 댓글