2025-09-29 Thread 심화, 람다 표현식

Ckd gus·2026년 2월 2일

Thread 동기화와 상태 제어, 데드락, 람다 표현식 정리

Thread 동기화

Thread 동기화는 여러 스레드가 공유 자원에 동시에 접근할 때 데이터 불일치 문제가 발생하지 않도록 관리하는 기술이다.

주요 동기화 방법으로는 synchronized가 있다.

class Counter {
    private int count = 0;

    // 동기화하지 않은 메서드 (문제 발생 가능)
    public void increment() {
        count++;  // 실제로는 읽기 → 증가 → 쓰기 과정
    }

    public int getCount() {
        return count;
    }
}
public class SynchronizedCounter {
    private int count = 0;

    // 동기화된 메서드
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter counter = new SynchronizedCounter();

        // 10개 스레드가 각각 1000번씩 증가
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        // 모든 스레드 종료 대기
        for (Thread t : threads) {
            t.join();
        }

        System.out.println("최종 카운트: " + counter.getCount());
    }
}

블록 동기화

public class BlockSynchronization {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        // 필요한 부분만 동기화
        synchronized (lock) {
            count++;
        }
    }
}

Thread 간 통신

Thread 간 통신은 여러 스레드가 협력하거나 데이터를 주고받기 위해 정보를 교환하는 기술을 의미한다.
주요 메서드는 wait, notify, notifyAll이며 생산자-소비자 패턴에서 많이 사용된다.

import java.util.ArrayList;
import java.util.List;

public class ProducerConsumer {
    private final List<Integer> buffer = new ArrayList<>();
    private final int MAX_SIZE = 5;
    private final Object lock = new Object();

    // 생산자
    public void produce(int value) throws InterruptedException {
        synchronized (lock) {
            while (buffer.size() >= MAX_SIZE) {
                System.out.println("버퍼가 가득 참, 대기 중...");
                lock.wait();
            }

            buffer.add(value);
            System.out.println("생산: " + value + ", 버퍼 크기: " + buffer.size());
            lock.notifyAll();
        }
    }

    // 소비자
    public int consume() throws InterruptedException {
        synchronized (lock) {
            while (buffer.isEmpty()) {
                System.out.println("버퍼가 비어있음, 대기 중...");
                lock.wait();
            }

            int value = buffer.remove(0);
            System.out.println("소비: " + value + ", 버퍼 크기: " + buffer.size());
            lock.notifyAll();
            return value;
        }
    }
}

Thread 상태 제어

Thread는 다음과 같은 상태를 가진다.

  • NEW: 스레드 객체 생성됨
  • RUNNABLE: 실행 가능 상태
  • BLOCKED: 동기화 블록 진입 대기
  • WAITING: 무기한 대기
  • TIMED_WAITING: 시간 제한 대기
  • TERMINATED: 종료됨

Thread 상태 관련 메서드로는 join, interrupt, 우선순위 설정 등이 있다.


join

join은 다른 스레드의 종료를 기다리는 메서드이다.

public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("작업 시작");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("작업 완료");
        });

        worker.start();

        System.out.println("작업자 스레드 종료 대기...");
        worker.join();
        System.out.println("모든 작업 완료!");
    }
}

interrupt

interrupt는 스레드를 안전하게 중단시키기 위한 요청이다.

public class InterruptExample {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    System.out.println("작업 진행: " + i + "/10");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                System.out.println("작업이 중단되었습니다!");
                return;
            }
            System.out.println("작업 완료");
        });

        worker.start();

        Thread.sleep(3500);
        worker.interrupt();
    }
}

우선순위 설정

우선순위는 스레드가 CPU 자원을 얼마나 자주 할당받는지에 영향을 주는 값이다.
1~10 사이의 정수로 지정하며 기본값은 5이다.

public class PriorityExample {
    public static void main(String[] args) {
        Thread highPriority = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("높은 우선순위: " + i);
            }
        });

        Thread lowPriority = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("낮은 우선순위: " + i);
            }
        });

        highPriority.setPriority(Thread.MAX_PRIORITY);
        lowPriority.setPriority(Thread.MIN_PRIORITY);

        highPriority.start();
        lowPriority.start();
    }
}

데드락(Deadlock)

데드락은 두 개 이상의 스레드가 서로가 가진 자원을 기다리면서 무한정 대기하는 상태이다.

데드락 발생 조건

  • 상호 배제: 리소스를 한 번에 한 스레드만 사용
  • 점유와 대기: 리소스를 점유하면서 다른 리소스를 대기
  • 비선점: 강제로 리소스를 빼앗을 수 없음
  • 순환 대기: 리소스 대기가 순환 구조
public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + ": lock1 획득");

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }

            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + ": lock2 획득");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println(Thread.currentThread().getName() + ": lock2 획득");

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }

            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + ": lock1 획득");
            }
        }
    }
}

데드락 해결 방법

락 순서 통일

public class DeadlockSolution {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            synchronized (lock2) {
                // 작업 수행
            }
        }
    }

    public void method2() {
        synchronized (lock1) {
            synchronized (lock2) {
                // 작업 수행
            }
        }
    }
}

타임아웃 설정

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

public class TimeoutSolution {
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public void performTask() throws InterruptedException {
        while (true) {
            if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            // 작업 수행
                            break;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            Thread.sleep(100);
        }
    }
}

람다 표현식

람다 표현식은 익명 함수를 간단하게 표현하는 문법이다.
기본 문법은 (매개변수) -> { 실행문 } 형태이다.

public class PlusFunctionExam {
    public static void main(String[] args) {
        PlusFunction plusObj = (int i, int j) -> {
            return i + j;
        };

        int value = plusObj.plus(100, 200);
        System.out.println(value);
    }
}

@FunctionalInterface
interface PlusFunction {
    int plus(int i, int j);
}

함수형 인터페이스 예시

@FunctionalInterface
interface Greeting {
    void sayHello();
}

@FunctionalInterface
interface StringProcessor {
    String process(String str);
}

@FunctionalInterface
interface Calculator {
    double calculate(double a, double b);
}

public class LambdaExamples {
    public static void main(String[] args) {
        Greeting greeting = () -> System.out.println("안녕하세요!");
        greeting.sayHello();

        StringProcessor upperCase = str -> str.toUpperCase();
        System.out.println(upperCase.process("hello"));

        Calculator multiply = (a, b) -> a * b;
        System.out.println(multiply.calculate(5.0, 3.0));

        Calculator complexCalc = (a, b) -> {
            double result = a + b;
            System.out.println("계산 중: " + a + " + " + b);
            return result;
        };
        System.out.println(complexCalc.calculate(10.0, 20.0));
    }
}

표준 함수형 인터페이스

import java.util.function.*;

public class StandardFunctionalInterfaces {
    public static void main(String[] args) {
        Predicate<Integer> isEven = num -> num % 2 == 0;
        System.out.println(isEven.test(4));
        System.out.println(isEven.test(5));

        Function<String, Integer> stringLength = str -> str.length();
        System.out.println(stringLength.apply("Hello"));

        Consumer<String> printer = str -> System.out.println("출력: " + str);
        printer.accept("람다식 테스트");

        Supplier<Double> randomSupplier = () -> Math.random();
        System.out.println("랜덤 값: " + randomSupplier.get());

        BinaryOperator<Integer> add = (a, b) -> a + b;
        System.out.println(add.apply(10, 20));
    }
}

느낀점

오늘은 빠르게 지나가서 쓰레드나 람다에서 궁금한 점들을 찾아보고 정리하느라 시간이 좀 걸린 것 같다.
쓰레드는 기능이 생각보다 많아서 전부 이해하는 데 시간이 걸릴 것 같다.

람다식은 자바스크립트에서 잠깐 사용해봤는데, 자바에서 사용해보니까 아직은 어색한 것 같다.
그래도 자주 사용해서 익숙해지면 엄청 편하게 사용할 수 있을 것 같다.

내일은 스트림을 공부하는데 재미있을 것 같다.

profile
백엔드 공부중입니다.

0개의 댓글