JAVA

태태개발일지 - 자바 고급 멀티쓰레드(김영한)

태태코 2025. 2. 22. 20:23
반응형

동시성

저번 글은 메모리 가시성에 대한 글을 작성했다. 보통 성능을 위해 변수를 메모리에서 직접 참조하지 않고, 캐시에 담아서 값을 처리를 하는데, 이를 통해서 생기는 문제였다. 이번에 다룰 부분은 동시성이다.

 

private Integer x = 7000



if(x > 5000){

   x -= 4000;

}

 

쓰레드 안에서 이러한 로직을 실행할 때 문제가 생긴다. 

 

만약 두개의 쓰레드가 동시에 실행한다면 어떻게 될까?

 

  1.  둘다 if문에 들어가서 검증로직을 수행 후 , x의 값을 변경하게 된다.
  2.  동시에 값을 참조했으므로 x의 값은 7000으로 찍히고 둘다 if문 안으로 들어가게 된다.
  3.  첫번째 쓰레드가 x에서 -4000을 해버렸고, 두번째 쓰레드가 값을 변경할때 x는 1000이 되게 되어 x는 음수가 되는 상황이 발생한다.

 

이러한 동시성 제어를 위해 나온 키워드가 synchronized이다.

synchronized

java에서 멀티쓰레드 환경에서 동기화를 보장하기 위해 사용되는 키워드이다.
이 키워드를 사용하면, 여러 개의 쓰레드가 하나의 자원에 접근하는 것을 막을 수 있다.

 

 

 public synchronized void increment() {
        count++;
    }

 

*모니터락:  각 쓰레드가 본인의 락을 가지고 있는데 그것을 모니터 락이라고 한다.*

 

 synchronized의 단점

 성능저하

  • 한 번에 하나의 쓰레드만 접근 가능하게 만들기 때문에 병령 처리가 제한된다.
  • 멀티쓰레드의 장점을 못 살릴 수 있다.

데드락 위험

  • 여러 개의 synchronized 블록이 서로 다른 객체를 잠그면서 교착상태에 빠질 수 있다.

분산환경 지원X

  • JVM 내부에서만 동작하기 때문에 분산 환경에서는 동기화가 봅장되지 않는다.
  • 분산 환경에서는 여러 서버에서 같은 자원에 접근할 수 있는데 synchronized 한 JVM 내부에서만 락을 관리하기 때문에
  • 다른 서버에서는 동기화가 적용되지않는다.

 

데드락 추가설명: synchronized에 들어가면 block 상태로 들어가는데 이는 인터럽트를 통해서도 상태를 변경시킬 수 없기 때문에 심각하다.

 

이를 보완하기 위해

 

 

LockSupport

LockSupport 의 park, unpark,parkNanos(밀리세컨)를 통해서 synchronized 단점을 해결 할 수는 있다

하지만, 개발자가 직접 로직 구현을 해야하기 때문에, 아래의 ReentrantLock를 사용하다.

 

 

 

ReentrantLock 

기본 구현.

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock(); // 락 객체 생성

    public void increment() {
        lock.lock();  //  락 획득
        try {
            count++;
        } finally {
            lock.unlock();  //  락 해제 (꼭 해줘야 함!)
        }
    }

    public int getCount() {
        return count;
    }
}

 

tryLock() 

synchronized는 락을 얻을 때까지 무조건 기다려야 하지만,

ReentrantLocktryLock()바로 락을 얻을 수 있는지 확인 가능하다.

if (lock.tryLock()) {  
    try {
        // 작업 실행
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("다른 쓰레드가 사용 중이므로 실행하지 않음");
}

 

 

lockInterruptibly()

보통 lock()을 걸면 쓰레드가 무조건 대기해야 하지만,

lockInterruptibly()을 사용하면 인터럽트 신호를 받을 수 있다.

 

try {
    lock.lockInterruptibly();  // 인터럽트 가능하게 락 획득
    // 작업 실행
} catch (InterruptedException e) {
    System.out.println("인터럽트 발생! 종료");
} finally {
    lock.unlock();
}

 

 Condition

synchronizedwait() & notify()처럼

ReentrantLock에서는 Condition 객체를 사용해서 대기 & 알림을 줄 수 있다.

 

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

public class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean available = false;

    public void produce() throws InterruptedException {
        lock.lock();
        try {
            available = true;
            condition.signal();  // 대기 중인 쓰레드 깨우기
        } finally {
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (!available) {
                condition.await();  // 조건 충족될 때까지 대기
            }
            available = false;
        } finally {
            lock.unlock();
        }
    }
}
반응형