hanker

JAVA - 멀티스레딩 가이드 (구현부터 동기화까지) 본문

JAVA

JAVA - 멀티스레딩 가이드 (구현부터 동기화까지)

hanker 2025. 1. 7. 00:00
반응형

Java 멀티스레딩(Multi-threading)은 하나의 프로그램에서 동시에 여러 작업을 실행할 수 있도록 하는 강력한 기능이다.

 

이번 글에서 Java 멀티스레딩을 사용하는 방법과 주요 개념, 실 예제까지 알아보자!


1. 멀티스레딩의 정의와 필요성

 

멀티스레딩은 하나의 프로세스 내에서 여러 스레드(Thread)가 동시에 실행되도록 하는 기술이다.

Java는 기본적으로 멀티스레드 환경을 지원한다.

 

사용이점 

1-1. 성능 향상 : 병렬 처리로 작업 시간을 줄일 수 있다.

1-2. 응답성(response time) 개선 : 긴 작업이 진행되는 동안에도 애플리케이션이 응답성을 유지할 수 있다.

1-3. 자원 효율성 : 동일한 메모리 공간에서 여러 작업을 수행하므로 자원을 더 효율적으로 사용할 수 있다.

 


2. Java의 스레드 모델

 

Java에서는 java.lang.Thread 클래스와 java.lang.Runnable 인터페이스를 사용하여 멀티스레드를 구현할 수 있다.

두 방식 모두 스레드의 실행 로직을 정의할 수 있지만, 상황에 따라 적합한 방법을 선택해야 한다.

 

2-1. Thread 클래스

Thread 클래스는 Java의 기본 스레드 구현체로, 직접 확장하여 스레드 동작을 정의할 수 있다.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running: " + Thread.currentThread().getName());
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start(); // 스레드 실행
    }
}

 

 

2-2. Runnable 인터페이스

Runnable 인터페이스는 스레드 동작을 정의하는 또 다른 방법으로, 스레드를 생성하지 않고 실행 로직만을 분리할 수 있다.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running: " + Thread.currentThread().getName());
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
    }
}

 


3. 주요 멀티스레딩 기능

 

3-1. 스레드 우선순위 설정

스레드는 우선순위를 가질 수 있으며, 이는 운영체제가 스레드를 실행하는 데 참고한다.

기본값은 5이며, 최소값은 1, 최대값은 10이다.

Thread thread = new Thread(() -> System.out.println("Running"));
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();

 

 

3-2. 스레드 동기화

멀티스레드 환경에서 여러 스레드가 동일한 자원을 접근하면 데이터 불일치 문제가 발생할 수 있다.

이를 방지하기 위해 synchronized 키워드를 사용한다.

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

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

        System.out.println("Final count: " + counter.getCount());
    }
}

 

 

3-3. 스레드 간 통신

스레드 간의 데이터 교환이나 작업 조정을 위해 wait(), notify(), notifyAll() 메서드를 사용할 수 있다.

- wait() : 현재 스레드를 대기 상태로 전환. 다른 스레드가 notify(), notifyAll()을 호출하기 전까지 대기 상태에 있다.

- notify() : 대기 중인 스레드 중 하나를 실행시킨다.

- notifyAll() : 대기 중인 모든 스레드를 실행시킨다.

class SharedResource {
    private boolean available = false;

    public synchronized void produce() throws InterruptedException {
        while (available) {
            wait();
        }
        System.out.println("Producing...");
        available = true;
        notify();
    }

    public synchronized void consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        System.out.println("Consuming...");
        available = false;
        notify();
    }
}

public class CommunicationExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    resource.produce();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    resource.consume();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

4. 그 외 멀티스레딩 기능

 

4-1. Executor 프레임워크

Java 5부터 추가된 Executor 프레임워크는 스레드 생성을 간소화하고, 스레드 풀(Thread Pool)을 관리할 수 있는 기능을 제공한다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executor.execute(() -> 
                System.out.println("Task executed by: " + Thread.currentThread().getName()));
        }

        executor.shutdown();
    }
}

 

 

4-2 멀티스레드에 적합한 컬렉션

멀티스레드 환경에서 안전하게 사용할 수 있는 컬렉션 클래스는 ConcurrentHashMap, CopyOnWriteArrayList이다.

 

- ConcurrentHashMap 

ConcurrentHashMap은 멀티스레드 환경에서 사용할 수 있도록 나온 HashMap의 스레드 안전 버전으로,

병렬 처리를 위해 내부적으로 여러 세그먼트로 나누어 동작한다. 이를 통해 여러 스레드가 동시에 읽고 쓸 수 있다.

 

- CopyOnWriteArrayList

CopyOnWriteArrayList는 읽기 작업이 빈번한 환경에서 안전하게 사용되는 리스트이다.

쓰기 작업이 발생할 때 내부 배열을 복사하여 동작한다.

 


정리

 

Java 멀티스레딩 기능은 올바르게 사용하지 않으면 디버깅이 어려운 문제를 초래할 수 있다.

기본적인 스레드 생성 방법 부터 동기화와 통신 기법, Executor 프레임워크까지 다양한 기능들을 숙지해서 올바른 멀티스레드 기능을 사용하길 바란다!

 

끝.

반응형