JAVA/JAVA 기초

Java Thread 활용

임베디드 친구 2024. 10. 11. 20:12
반응형

1. Thread란?

Thread는 프로세스 내에서 실행되는 작은 실행 단위로, 독립적으로 동작할 수 있는 최소 단위입니다. 하나의 프로세스 안에서 여러 스레드가 동작하여 병렬로 작업을 수행할 수 있으며, 이로 인해 멀티태스킹 및 병렬 처리가 가능합니다. 쉽게 설명하자면, 하나의 프로그램에서 독립된 여러 개의 작업이 동시에 실행될 수 있게 하는 것이 Thread의 주된 역할입니다.

Java에서는 Thread를 사용하여 복잡한 작업을 백그라운드에서 수행하거나, UI를 블로킹하지 않도록 작업을 분산시키는 등의 기능을 구현할 수 있습니다.

2. Thread 생성 방법

Java에서 Thread를 생성하는 방법은 크게 두 가지로 나뉩니다.

2.1 Thread Class 상속

Thread 클래스를 상속받아 자식 클래스를 구현하는 방법입니다. 이 방법은 비교적 간단하며, 스레드 클래스 내부에 run() 메서드를 재정의하여 실행하고자 하는 코드를 작성합니다.

class ExThread extends Thread {
    public void run() {
        // 스레드가 실행할 코드 작성
        System.out.println("Hello from ExThread!");
    }
}

public class Main {
    public static void main(String[] args) {
        ExThread exThread = new ExThread();
        exThread.start(); // 스레드 시작
    }
}

위 예제에서 start() 메서드를 호출하면 ExThread의 run() 메서드가 실행됩니다. start() 메서드는 새로운 스레드를 생성하고 run() 메서드를 호출하여 병렬로 작업을 수행합니다.

2.2 Runnable Interface 구현

Runnable 인터페이스를 구현한 클래스를 사용하여 Thread 객체를 생성하는 방법입니다. Runnable 인터페이스를 사용하면 클래스가 다른 클래스도 상속받을 수 있어 보다 유연한 설계가 가능합니다.

class ExRunnable implements Runnable {
    public void run() {
        // 스레드가 실행할 코드 작성
        System.out.println("Hello from ExRunnable!");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread exThread = new Thread(new ExRunnable());
        exThread.start(); // 스레드 시작
    }
}

이 예제에서는 Thread 객체가 Runnable을 구현한 ExRunnable 객체를 인자로 받아 생성됩니다. 이후 start()를 호출하여 스레드를 시작합니다.

3. 동기화(Synchronization)

여러 스레드가 공유된 자원에 접근할 때는 동기화(Synchronization)가 필요합니다. 동기화를 통해 스레드 간의 충돌을 방지하고, 안전한 실행을 보장할 수 있으며, 데이터의 무결성을 유지할 수 있습니다.

Java에서는 synchronized 키워드를 사용하여 메서드 또는 블록을 동기화할 수 있습니다. 동기화는 임계 영역(Critical Section)을 만들어 동시에 여러 스레드가 접근하는 것을 막는 역할을 합니다.

3.1 synchronized 블록

동기화 블록은 다음과 같이 사용할 수 있습니다.

class Counter {
    private int count = 0;

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

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

class CounterThread extends Thread {
    private Counter counter;

    public CounterThread(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
            counter.decrement();
        }
    }
}

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

        // 두 개의 스레드가 동일한 자원에 접근
        CounterThread thread1 = new CounterThread(counter);
        CounterThread thread2 = new CounterThread(counter);

        // 스레드 시작
        thread1.start();
        thread2.start();

        // 스레드 종료 대기
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 최종 카운트 출력
        System.out.println("Final count: " + counter.getCount());
    }
}

위 예제에서 increment()와 decrement() 메서드는 synchronized 키워드를 사용하여 동기화되어 있습니다. 이로 인해 두 스레드가 동시에 count를 수정하려고 할 때 동기화 메커니즘이 작동하여 데이터의 무결성을 유지하게 됩니다.

주의: synchronized를 과도하게 사용하면 성능이 저하될 수 있습니다. 동기화로 인해 스레드 간의 병목(Bottleneck) 현상이 발생할 수 있기 때문에, 필요한 부분에만 최소한으로 사용하는 것이 좋습니다.

4. Thread Pool 사용하기

Thread Pool은 많은 애플리케이션에서 스레드를 효율적으로 관리하기 위해 사용됩니다. 스레드 풀은 미리 생성된 스레드 집합을 관리하고, 작업을 스레드에 할당하여 병렬 처리를 쉽게 구현할 수 있습니다. Java에서는 Executor 인터페이스와 Executors 유틸리티 클래스를 통해 스레드 풀을 사용할 수 있습니다.

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

class Task implements Runnable {
    private final int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task ID: " + taskId + " performed by " + Thread.currentThread().getName());
    }
}

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 3개의 스레드로 구성된 풀 생성

        // 10개의 작업을 제출
        for (int i = 1; i <= 10; i++) {
            executor.execute(new Task(i));
        }

        // 모든 작업이 완료된 후 스레드 풀 종료
        executor.shutdown();
    }
}

이 예제에서는 FixedThreadPool을 사용하여 최대 3개의 스레드만을 가지는 스레드 풀을 생성했습니다. 이후 10개의 작업을 제출하였고, 스레드 풀은 내부적으로 스레드를 할당하여 작업을 병렬로 처리하게 됩니다.

5. Thread 사용의 이점

Thread를 사용하여 얻을 수 있는 이점은 다음과 같습니다:

  1. 멀티태스킹: 여러 작업을 동시에 실행하여 프로그램의 성능을 향상시키고, 시스템 자원을 효율적으로 활용할 수 있습니다.
  2. 응답성 향상: UI와 관련된 프로그램에서는 긴 연산을 별도의 스레드에서 수행하여 사용자 인터페이스의 응답성을 높일 수 있습니다.
  3. 자원 공유: 스레드를 사용하면 메모리와 같은 자원을 효과적으로 공유할 수 있으며, 동기화 메커니즘을 통해 안전한 액세스를 보장할 수 있습니다.
  4. 모듈화된 코드: 복잡한 작업을 스레드로 나누어 코드의 모듈화 및 유지보수를 쉽게 할 수 있습니다.
  5. 멀티코어 시스템 활용: 현대 컴퓨터의 멀티코어 프로세서를 효과적으로 활용하여 성능을 최적화할 수 있습니다.
  • 비동기 프로그래밍: 스레드를 이용하면 비동기 작업을 구현하여 I/O 작업이나 네트워크 통신과 같은 블록킹 작업을 효율적으로 처리할 수 있습니다.

6. Thread 사용 시 주의사항

Thread 사용 시 발생할 수 있는 문제로는 다음과 같은 사항들이 있습니다:

  1. 경쟁 조건(Race Condition): 두 개 이상의 스레드가 같은 자원에 동시 접근하여 발생하는 문제입니다. 동기화를 통해 해결할 수 있습니다.
  2. 데드락(Deadlock): 두 개 이상의 스레드가 서로의 자원에 대해 무한히 기다리는 상태입니다. 이 문제를 피하기 위해 동기화 설계를 주의해야 합니다.
  3. 스레드 풀의 적절한 크기 설정: 스레드 풀의 크기를 적절하게 설정하지 않으면 오버헤드가 발생하거나 과도한 컨텍스트 스위칭으로 성능이 저하될 수 있습니다.

7. 결론

Java의 Thread는 멀티태스킹을 구현하고 프로그램의 성능을 최적화하는 데 매우 유용한 도구입니다. Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하여 스레드를 생성하고, synchronized 키워드를 통해 동기화 문제를 해결할 수 있습니다. 또한, Thread Pool을 사용하여 효율적으로 스레드를 관리하고, 병렬 작업을 쉽게 처리할 수 있습니다.

그러나 스레드를 사용할 때는 경쟁 조건, 데드락 등의 문제를 피하기 위해 주의 깊게 설계해야 하며, 불필요한 동기화 사용을 피하여 성능을 최적화할 필요가 있습니다.

위의 예제와 설명을 통해 Java의 Thread에 대한 이해를 돕고, 실무에서 효과적으로 활용할 수 있는 방법을 익힐 수 있기를 바랍니다.

반응형