고급동기화

수호천사임다·2024년 10월 21일

자바

목록 보기
14/15

고급 동기화

  • 자바에서 기본적인 synchronized 키워드 이상의 동기화 방법을 사용하여 보다 복잡한 멀티스레드 환경에서의 문제를
    해결하는 방법들.
  • 고급동기화의 핵심은 자원을 효울적으로 관리하면서 데드락, 라이브락, 성능 저하 등의 문제를 최소하 하는 것.

synchronized 단점

  • 무한 대기: BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기.
    • 특정 시간까지만 대기하는 타임아웃이 없으며, 중간에 인터럽트도 불가능하다.
  • 공정성: 락이 들어왔을 떄 BLOKCED 상태의 여러 스레드 중에 어떤 스레드가
    락을 획득 했는지 알 수 없다. 최악의 경우 스레드가 너무 오랜기간 락을 획득하지 못할 수도 있다.

LockSupport

  • 자바에서 제공하는 저수준의 스레드 동기화 도구로, 스레드를 효울적으로 차단(block)하고 꺠울(unblock)수 있다.
  • 스레드를 WAITING 상태로 변경.
    • WAITING 상태는 스레드가 특정 조건이나 시간 동안 대기할 때 발생하는 상태(누군가 꺠워줘야 한다.).
  • CPU 실행 스케줄링에 들어기지 않는다.

LockSupport 메서드

메서드설명시그니처
park()현재 스레드를 차단 (다른 스레드가 unpark() 호출 시까지 대기)public static void park()
unpark(Thread thread)특정 스레드를 깨움 (해당 스레드가 park() 상태일 경우 차단 해제)public static void unpark(Thread thread)
parkNanos(long nanos)지정된 나노초 동안 스레드를 차단public static void parkNanos(long nanos)
parkUntil(long deadline)특정 시간(밀리초 기준)까지 스레드를 차단public static void parkUntil(long deadline)
park(Object blocker)차단될 때의 이유 또는 차단 객체를 지정하여 스레드를 차단public static void park(Object blocker)
LockSupport 예시코드
package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;

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

public class LockSupportMain {

public static void main(String[] args) {

  Thread thread1 = new Thread(new ParkTest(), "Thread-1");
  thread1.start();

  // 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다.
  sleep(100);
  log("Thread-1 state: " + thread1.getState());

  log("main -> unpark(Thread-1)");
  LockSupport.unpark(thread1); // 1. unpark 사용
  //thread1.interrupt(); // 2. interrupt() 사용

}

static class ParkTest implements Runnable {

  @Override
  public void run() {
    log("park 시작");

    LockSupport.park(); // Runnable -> WAITING
    //LockSupport.parkNanos(2000_000000); // parkNanos 사용
    log("park 종료, state " + Thread.currentThread().getState());
    log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
  }
}
}

LockSupport를 사용하면 스레드는 WAITING, TIMED_WAITING 상태로 변경할 수 있고,
interrupt를 받아서 스레드를 깨울 수도 있다. -> synchronized의 단점인 무한 대기 문제 해결 가능
LockSupport를 활용하면, 무한 대기하지 않는 락 기능을 만들 수 있다.

LockSupport 활용
if(!lock.tryLock(10)) { // 내부에서 parkNanos() 사용
  log("[진입 실패] 너무 오래 대기했습니다.");
  return false;
}

// 임계 영영 시작
      ...
// 임계 영역 종료
lock.unlock(); // 내부에서 unpark() 사용

락(lock)이라는 클래스를 만들고, 특정 스레드가 먼저 락을 얻으면 RUNNABLE로 실행하고,
락을 얻지 못하면 park()를 사용해서 대기 상태로 만든다. 스레드가 임계 영역의 실행을 마치고 나면 락을
반납하고, unpark()를 사용해서 대기중인 다른 스레드를 깨운다.

-> 이런 기능을 직접 구현하기는 매우 어렵다.

Lock 인터페이스

  • 동시성 프로그래밍에서 쓰이는 안전한 임계 영역을 위한 락을 구현하는데 사용.
  • Lock 인터페이스에서 사용하는 락은 객체 내부에 있는 모니터 락이 아닌, Lock 인터페이스와
    ReentrantLock이 제공하는 기능.
  • Lock 인터페이스와 ReentrantLock 구현체를 사용하면 synchronized 단점인 무한 대기와 공정성 문제를 모두 해결할 수 있다.

Lock 메서드

메서드설명
void lock()스레드가 락을 획득할 때까지 대기 (즉, 락이 사용 가능해질 때까지 차단)
void lockInterruptibly() throws InterruptedException락을 획득할 때 인터럽트가 발생하면 예외를 던짐 (대기 중 인터럽트를 처리할 수 있음)
boolean tryLock()락을 획득하려 시도하고, 락이 사용 중일 경우 즉시 false 반환
boolean tryLock(long time, TimeUnit unit) throws InterruptedException주어진 시간 동안 락을 시도하고, 시간이 초과되면 false 반환
void unlock()락을 해제하여 다른 스레드가 획득할 수 있게 함
Condition newCondition()Condition 객체를 생성하여 락과 함께 사용할 수 있게 함
메서드 예시

void lock() - 맛집에 한번 줄을 서면 끝가지 기다린다. 친구가 다른 맛집을 찾았다고 중간에
연락해도 포기하지 않고 기다린다.

void lockInterruptibly() - 맛집에 한번 줄을 서서 기다린다. 다만 친구가 다른 맛집을 찾았다고 중간에 연락하면 포기한다.

boolean tryLock() - 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기한다.

boolean tryLock(long time, TimeUnit unit) - 맛집에 줄을 서지만 특정 시간 만큼만 기다린다.
특정 시간이 지나면 계쏙 줄을 서야 한다면 포기한다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기한다.

void unlock() - 식당안에 있는 손님이 밥을 먹고 나간다. 식당에 자리가 하나 난다. 기다리라는 손님께
이런 사실을 알려주어야 한다. 기다리던 손님중 한명이 식당에 들어간다.

ReentrantLock

자바는 1.0부터 존재한 synchronizedBLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해
자바 1.5부터 Lock 인터페이스와 ReentrantLock 구현체를 제공

  • 스레드가 공정하게 락을 얻을 수 있는 모드를 제공
  • 공정성(fairness) 모드와 비공정(non-fair)모드로 설정할 수 있으며, 이 두 모드는
  • 락을 획득하는 방식에서 차이가 있다.

비공정 모드(Non-fair mod)

  • ReentrantLock의 기본 모드.
  • 락을 먼저 요청한 스레드가 락을 획득한다는 보장 x
  • 락을 풀었을 때, 대기 중인 스레드 중 아무나 락을 획득 할 수 있다.
  • 락을 빨리 획들할 수 있지만, 특정 스레드가 장기간 락을 획득하지 못할 가능성도 있다.

비공정 모드 특징

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

공정 모드(Fair mode)

  • new ReentranLock(true); -> 공정모드
  • 락을 요청한 순서대로 스레드가 락을 획들할 수 있게 한다.
  • 먼저 대기한 스레드가 먼저 락을 획득하게 되어 스레드 간의 공정성을 보장.
  • 성능 저하가 될 수 있다.

공정 모드 특징

  • 공정성 보장: 대기 큐에서 대기한 스레드가 락을 먼저 획득.
  • 기아 현상 방지: 모든 스레드가 언젠가 락을 획들할 수 있게 보장.
  • 성능 저하: 락을 획득하는 속도가 느려질 수 있다.
public class ReentrantLock {
  // 비공정 모드 락
  private final Lock nonFairLock = new ReentranLock();
  // 공정 모드 락 
  private final Lock nonFairLock = new ReentranLock(true);
}
ReentrantLock 활용
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-finally 사용

  try {
    // ==임계 영영 시작==
    log("[검증 시작] 출금액 : " + amount + ", 잔액: " + balance);
    if(balance < amount){
      log("[검증 실패]" + "출금액 : " + amount + ", 잔액: " + balance);
      return false;
    }

    log("[검증 완료] 출금액 : " + amount + ", 잔액: " + balance);
    sleep(1000);
    balance -= amount;
    log("[출금 완료] 출금액 : " + amount + ", 잔액: " + balance);
    // ==임계 영영 종료==
  } finally {
    lock.unlock();
  }
  log("거래 종료");
  return true;
}

@Override
public int getBalance() {
  lock.lock(); // ReentrantLock 이용하여 lock을 걸기
  try {
    return balance;
  } finally {
    lock.unlock(); // ReentrantLock 이용하여 lock 해제
  }
}
}
if(!lock.tryLock(500, TimeUnit.MILLISECONDS)) {  // 시간
  log("[진입 실패] 이미 처리중인 작업이 있습니다.");
  return false;
}
  • lock() -> unlock() 까지는 안전한 임계 영역이 된다.
  • 임계 영역이 끝나면 반드시 락을 반납해야 한다. 그렇지 않으면 대기하는 스레드가 락을 얻지 못한다.
  • unlock()은 반드시 finally 블럭에 작성.

synchronized의 단점을 해결해줄 LockSupport가 있지만 실제로 구현하기에는 어려운 부분이 있기 때문에
단순하게 편리하게 사용하기에는 synchronized가 좋아보이지만 더 세밀한 동기화 제어와
향상된 성능, 비차단락 획득, 조건 대기 및 신호, 경량화된 동기화, 데드락 회피, 공정성 보장,
인터럽트 가능한 락 대기의 기능을 사용하고싶다면 고급동기화 Lock 인터페이스와 ReentantLock 구현체를 사용해야 한다.
고급동기화를 배움으로서 멀티스레드에 대한 이해가 잘 되고 있는 거 같다.

예제 출처: 김영한의 실전 자바 - 고급1편, 멀티스레드와 동시성

0개의 댓글