Lock 오브젝트
Lock 오브젝트 기본 개념
Java는 락의 정교한 컨트롤을 위해서 Lock 인터페이스를 제공한다.
Lock 객체는 synchronized의 내부 락과 유사하게 동작한다.
Lock 객체는 내부 락과 같이 오직 하나의 스레드만 Lock 객체에 대한 소유권을 가질 수 있다.
public class Main{
private static Lock lock = new ReentrantLock();
private static int count = 0;
private static void increment(){
try{
lock.lock();
count+=1;
}finally{
lock.unlock();
}
}
public static void main(String args[]){
Runnable r = Main::increment;
new Thread(r).start();
}
}
또한 Condition 오브젝트를 통한 wait / notify 기능도 제공한다.
public class Main{
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static boolean ready = false;
private static void awaitReady(){
try{
lock.lock();
while(!ready){
// wait
condition.await();
}
}finally{
lock.unlock();
}
}
private static void signalAll(){
try{
lock.lock();
// notify all
condition.signalAll();
}finally{
lock.unlock();
}
}
public static void main(String args[]){
Runnable r = Main::awaitReady;
new Thread(r).start();
signAll();
}
}
Lock vs Synchronized
Synchronized의 내부 락과 달리, Lock 객체는 락 획득 실패 시 무한대기하지 않고 빠져나올 수 있다.
Lock의 tryLock 메서드는 락을 즉시 획득할 수 없거나 일정 시간동안 획득하지 못했다면 무한대기하지 않고 빠져나올 수 있도록 기능을 제공한다
Lock의 lockInterruptibly 메서드는 락을 획득하기 위해 무한대기하지만, 다른 스레드가 인터럽트를 보내면 기다리는 것을 중단하고 빠져나올 수 있다.
tryLock과 lockInterruptibly 모두 데드락 문제에 빠지는 것을 어느정도 예방하기에 유용하다.
Executor
기본 개념
기존의 직접 Thread를 생성하는 방식은 Runnable에서 작업을 정의하고 Thread.start로 작업을 실행했다.
하지만, 대규모 애플리케이션에서는 스레드 관리와 작업의 정리가 밀접하게 연결된 기존 방식을 사용하는 것은 좋지 않다
이러한 상황에서 Executors는 작업과 스레드를 분리해서 관리하기 위해 사용할 수 있는 적절한 기술이다.
Executor 인터페이스
Executor 인터페이스는 execute라는 단 한 개의 메서드만을 지원한다.
해당 메서드는 기존의 Thread 기반의 방식처럼 어떤 작업(Runnable)을 실행하는 역할을 한다.
public static void main(String args[]){
Runnable runnable = () -> System.out.println("run");
// Thread 방식
new Thread(r).start();
// executor 방식
executor.execute(r);
}
하지만 두 방식의 차이점은 명확하다
- Thread 방식 : 매번 작업을 실행할 때마다 새로운 스레드를 생성함
- executor 방식 : 기존에 생성한 스레드를 재사용함
따라서 executor 방식이 Thread 방식에 비해 시스템 자원을 효율적으로 관리할 수 있는 방식이다.
ExecutorService 인터페이스
ExecutorService는 execute뿐만 아니라 submit 메서드도 함께 제공한다.
submit 메서드는 Runnable 혹은 Callable을 인자로 받아서 Future를 반환한다.
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try{
Runnable r = () -> System.out.println("hello");
Callable<Integer> c = () -> 1;
List<Callable<Integer>> lc = List.of(
() -> 1,
() -> 2,
() -> 3,
);
Future<?> f1 = executor.submit(r);
Future<Integer> f2 = executor.submit(c);
List<Future<Integer>> f3 = executor.invokeAll(lc);
executor.shutdown();
}finally{
executor.shutdownNow();
}
}
}
워 코드에서처럼 Callable을 인자로 넘길 수 있기에 반환값이 있는 작업도 실행할 수 있다.
또한 Future를 반환값으로 받기에 Callable의 결과값을 확인하거나 작업의 상태를 관리할 수 있다.
또한 invokeAll 메서드를 통해 여러개의 Callable을 한 번에 처리할 수 있다.
해당 메서드는 Future<T>의 리스트를 리턴값으로 반환한다.
ExceutorService는 shutdown과 shutdownNot를 통해서 Executor의 종료를 관리할 수 있다.
shutdown은 더 이상 작업을 받지 않고 현재 작업이 종료된 후 Executor를 종료한다,
shutdownNow는 작업에 상관없이 바로 Executor를 종료한다.
ScheduledExecutorService
ScheduledExecutorService는 작업을 주기적으로 실행하거나 일정 시간 후 실행할 때 사용하면 좋다.
- schedule : 일정 시간 후 한 번만 작업을 실행
- scheduleAtFixedRate : 지정한 지연 시간마다 작업을 반복 실행
- 작업을 시작 후 지연 카운터가 돌아가기에 이전 작업이 끝나지 않았더라도 새로운 작업이 생성될 수 있음
- scheduleWithFixedDelay : 작업이 종료된 후 지정한 지연 시간이 지나고 작업을 반복 실행
- 작업이 종료된 후 지연 카운터가 돌아감
Thread Pool
대부분의 Executor 구현체는 워커 스레드로 구성된 스레드 풀을 사용한다.
워커 스레드는 runnable, callable과 같은 작업들과 독립적으로 존재하는 스레드로 필요한 시점에 작업을 실행하기 좋다.
워커 스레드의 스레드 재사용
워커 스레드는 미리 생성된 스레드를 재사용하기에 스레드 생성 / 제거 오버헤드를 최소화한다.
따라서 대규모 애플리케이션에서 스레드 생성 / 제거로 인한 메모리 오버헤드를 줄이는데 도움이 된다.
스레드 풀의 내부 큐
거의 모든 스레드 풀은 내부 큐를 통해서 작업을 관리한다.
따라서 스레드 수보다 더 많은 작업이 있을 때도 안정적으로 작업을 관리할 수 있다.
스레드 풀 종류
Executors는 대표적으로 다음과 같은 스레드 풀을 제공한다.
- Executors.newFixedThreadPool
- 항상 일정 수의 스레드를 유지하는 스레드 풀을 생성함
- 하나의 스레드가 실행 중 종료되었다면, 자동으로 새로운 스레드를 생성하여 개수를 유지할 수 있음
- Executors.newCachedThreadPool
- 확장 가능한 스레드 풀을 생성함
- 짧은 작업을 여러개 실행해야하는 상황에서 유용하게 사용할 수 있음
- Executors.newSingleThreadExecutor
- 한 번에 하나의 작업만을 수행할 수 있는 스레드 풀을 생성함
마무리
이번에는 효과적으로 락을 관리할 수 있는 Lock 객체를 알아보았다.
또한 효과적으로 스레드와 작업을 관리할 수 있는 Executor 인터페이스에 대해서 알아보았다.
이전에 간단하게 Executor를 활용하여 작업을 관리하는 방법에 대해서 다루었는데 직접 공식 문서를 통해서 보다 세밀하게 공부하니 보다 효과적으로 사용할 수 있는 방법에 대해서 알게 된 것 같다.
'CS 및 기본 개념' 카테고리의 다른 글
| [ cs ] Object Oriented Method (1) | 2025.09.10 |
|---|---|
| [ cs ] 객체 지향 프로그래밍( OOP ) 4가지 특징 (1) | 2025.09.04 |
| [Java 공식문서] 불변 객체 (4) | 2025.08.15 |
| [cs] Cache Control 헤더 (4) | 2025.08.13 |
| [ Java 공식문서 ] Liveness와 Guarded Block (6) | 2025.08.11 |