자바

자바 쓰레드

thebasics 2024. 8. 27. 17:00

목차
1. 쓰레드란 무엇인가?
2. 멀티쓰레딩의 중요성
3. 쓰레드의 기본 개념
   - 프로세스와 쓰레드
   - 자바에서의 쓰레드
4. 쓰레드 생성 방법
   - Thread 클래스를 상속하는 방법
   - Runnable 인터페이스를 구현하는 방법
   - 익명 클래스와 람다 표현식 사용
5. 쓰레드 제어
   - 쓰레드 시작, 일시 정지, 재개, 종료
   - 쓰레드 우선순위
   - join() 메서드
6. 쓰레드 동기화
   - synchronized 키워드
   - synchronized 블록
   - volatile 키워드
7. 쓰레드 간의 통신
   - wait()와 notify() 메서드
   - 쓰레드 풀
8. 멀티쓰레딩에서 발생할 수 있는 문제
   - 교착 상태 (Deadlock)
   - 경쟁 상태 (Race Condition)
9. 예제와 분석
10. 결론 및 추가 학습 자료


1. 쓰레드란 무엇인가?

쓰레드(Thread)는 프로세스 내에서 실행되는 가장 작은 단위의 작업 흐름을 의미합니다. 하나의 프로세스는 여러 개의 쓰레드를 가질 수 있으며, 이들은 서로 독립적으로 실행될 수 있습니다. 자바에서 쓰레드를 사용하면 동시에 여러 작업을 수행할 수 있어, 프로그램의 성능을 향상시킬 수 있습니다.


2. 멀티쓰레딩의 중요성

멀티쓰레딩(Multithreading)은 여러 개의 쓰레드를 동시에 실행함으로써 작업을 병렬로 처리하는 기법입니다. 이는 CPU 사용률을 극대화하고, 응답성을 향상시키며, 특히 병렬 작업이 필요한 경우(예: 서버 처리, 대량 데이터 처리)에서 매우 유용합니다.

멀티쓰레딩의 주요 장점은 다음과 같습니다:
- 병렬 처리: 여러 작업을 동시에 처리하여 성능을 향상시킵니다.
- 응답성 향상: 사용자 인터페이스(UI)가 블로킹되지 않도록 함으로써 응답성을 높입니다.
- 자원 효율성: 여러 쓰레드가 메모리와 자원을 공유함으로써 효율적인 자원 사용이 가능합니다.


3. 쓰레드의 기본 개념

쓰레드를 이해하기 위해서는 프로세스와 쓰레드의 차이, 그리고 자바에서의 쓰레드 개념을 이해하는 것이 중요합니다.

프로세스와 쓰레드

- 프로세스: 운영체제에서 실행 중인 프로그램의 인스턴스로, 독립된 메모리 공간을 가지고 있습니다.
- 쓰레드: 프로세스 내에서 실행되는 작업 단위로, 같은 프로세스 내에서 다른 쓰레드와 메모리 공간을 공유합니다. 이는 메모리 사용을 줄이고, 더 빠른 작업 전환이 가능하게 합니다.

자바에서의 쓰레드

자바에서는 'Thread' 클래스를 사용하여 쓰레드를 생성하고 관리합니다. 쓰레드를 생성하기 위한 두 가지 주요 방법이 있습니다: 'Thread' 클래스를 상속하는 방법과 'Runnable' 인터페이스를 구현하는 방법입니다.


4. 쓰레드 생성 방법

자바에서 쓰레드를 생성하는 방법에는 여러 가지가 있습니다. 각 방법은 쓰레드를 생성하고 실행하는 데 있어 조금씩 다른 특성을 가지고 있습니다.

Thread 클래스를 상속하는 방법

'Thread' 클래스를 상속받아 새로운 쓰레드를 생성할 수 있습니다. 이 방법에서는 'run()' 메서드를 오버라이드하여 쓰레드가 실행할 코드를 정의합니다.

예제 코드:

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

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

설명:
- 'MyThread' 클래스는 'Thread' 클래스를 상속받아 'run()' 메서드를 오버라이드합니다.
- 'start()' 메서드를 호출하면 새로운 쓰레드가 생성되고, 'run()' 메서드 내의 코드가 실행됩니다.

Runnable 인터페이스를 구현하는 방법

'Runnable' 인터페이스를 구현하는 방법은 쓰레드를 생성하는 또 다른 방법으로, 특히 다중 상속을 피해야 할 때 유용합니다.

예제 코드:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }
}

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

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

설명:
- 'MyRunnable' 클래스는 'Runnable' 인터페이스를 구현하고, 'run()' 메서드를 오버라이드합니다.
- 'Thread' 클래스의 인스턴스를 생성할 때 'Runnable' 객체를 전달하여 쓰레드를 실행합니다.

익명 클래스와 람다 표현식 사용

익명 클래스나 람다 표현식을 사용하면 간단한 쓰레드를 더 간결하게 생성할 수 있습니다.

예제 코드:

public class LambdaThreadExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " is running: " + i);
            }
        });

        thread1.start(); // 쓰레드 시작
    }
}

설명:
- 람다 표현식을 사용하여 간단히 'Runnable' 인터페이스를 구현하고, 쓰레드를 실행할 수 있습니다.


5. 쓰레드 제어

쓰레드를 제어하는 다양한 방법이 있습니다. 여기에는 쓰레드의 시작, 일시 정지, 재개, 종료, 그리고 우선순위 설정 등이 포함됩니다.

쓰레드 시작, 일시 정지, 재개, 종료

- 시작: 'start()' 메서드를 사용하여 쓰레드를 시작합니다.
- 일시 정지: 'sleep()' 메서드를 사용하여 쓰레드를 일정 시간 동안 일시 정지할 수 있습니다.
- 재개: 'sleep()' 메서드 호출 후 쓰레드는 자동으로 재개됩니다.
- 종료: 쓰레드의 'run()' 메서드가 완료되면 쓰레드는 자동으로 종료됩니다. 명시적으로 'interrupt()' 메서드를 호출하여 쓰레드를 종료할 수도 있습니다.

예제 코드:

class ControlledThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " is running: " + i);
                Thread.sleep(1000); // 1초 동안 일시 정지
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " was interrupted.");
        }
    }
}

public class ThreadControlExample {
    public static void main(String[] args) {
        ControlledThread thread = new ControlledThread();
        thread.start();

        try {
            Thread.sleep(3000);
            thread.interrupt(); // 3초 후에 쓰레드 종료 시도
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

설명:
- 'sleep()' 메서드를 사용하여 쓰레드를 일정 시간 동안 일시 정지하고, 'interrupt()' 메서드를 사용하여 쓰레드를 종료할 수 있습니다.

쓰레드 우선순위

쓰레드의 우선순위를 설정하여 스케줄러가 쓰레드를 처리하는 순서를 제어할 수 있습니다. 우선순위는 1(최저)에서 10(최고)까지 설정할 수 있으며, 기본값은 5입니다.

예제 코드:

class PriorityThread extends Thread {
    public PriorityThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + " is running with priority " + getPriority());
        }
    }
}

public class ThreadPriorityExample {
    public static void main(String[] args) {
        PriorityThread highPriorityThread = new PriorityThread("High Priority");
        PriorityThread lowPriorityThread = new PriorityThread("Low Priority");

        highPriorityThread.setPriority(Thread.MAX

_PRIORITY); // 우선순위 10
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 우선순위 1

        highPriorityThread.start();
        lowPriorityThread.start();
    }
}

설명:
- 'setPriority()' 메서드를 사용하여 쓰레드의 우선순위를 설정할 수 있습니다.

join() 메서드

'join()' 메서드는 하나의 쓰레드가 다른 쓰레드의 작업이 끝날 때까지 기다리도록 할 수 있습니다. 이를 통해 쓰레드 간의 작업 순서를 제어할 수 있습니다.

예제 코드:

class JoinThread extends Thread {
    public JoinThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + " is running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadJoinExample {
    public static void main(String[] args) {
        JoinThread thread1 = new JoinThread("Thread 1");
        JoinThread thread2 = new JoinThread("Thread 2");

        thread1.start();
        try {
            thread1.join(); // thread2는 thread1이 끝날 때까지 기다림
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread2.start();
    }
}

설명:
- 'join()' 메서드를 사용하여 'thread2'가 'thread1'의 작업이 완료될 때까지 기다리도록 설정합니다.


6. 쓰레드 동기화

멀티쓰레딩 환경에서는 여러 쓰레드가 같은 자원에 접근할 때 발생할 수 있는 문제를 방지하기 위해 동기화가 필요합니다. 자바에서는 'synchronized' 키워드를 사용하여 동기화를 구현할 수 있습니다.

synchronized 키워드

'synchronized' 키워드를 메서드나 블록에 적용하여, 한 번에 하나의 쓰레드만 해당 코드 블록을 실행할 수 있도록 할 수 있습니다.

예제 코드:

class Counter {
    private int count = 0;

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

    public 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();
        }
    }
}

public class SynchronizedExample {
    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()' 메서드에 'synchronized' 키워드를 사용하여 여러 쓰레드가 동시에 접근할 때 발생할 수 있는 문제를 방지합니다.

synchronized 블록

'synchronized' 블록은 특정 코드 블록만 동기화할 때 사용합니다. 이는 더 세밀하게 동기화할 수 있으며, 성능을 최적화할 수 있습니다.

예제 코드:

class BlockCounter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

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

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

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

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

설명:
- 'synchronized' 블록을 사용하여 'increment()' 메서드의 특정 부분만 동기화함으로써 성능을 최적화합니다.

volatile 키워드

'volatile' 키워드는 변수를 여러 쓰레드에서 읽고 쓸 때 메모리 가시성을 보장합니다. 이는 동기화하지 않고도 최신 값을 읽을 수 있도록 합니다.

예제 코드:

class VolatileExample {
    private volatile boolean running = true;

    public void stopRunning() {
        running = false;
    }

    public void startRunning() {
        while (running) {
            System.out.println("Running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Stopped.");
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        Thread thread = new Thread(example::startRunning);
        thread.start();

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

        example.stopRunning();
    }
}

설명:
- 'volatile' 키워드를 사용하여 'running' 변수가 항상 최신 값을 가지도록 보장합니다.


7. 쓰레드 간의 통신

쓰레드 간의 통신을 통해 쓰레드들이 서로 협력하여 작업을 수행할 수 있습니다. 이를 위해 'wait()', 'notify()', 'notifyAll()' 메서드를 사용합니다.

wait()와 notify() 메서드

'wait()' 메서드는 쓰레드를 일시 정지시키고, 'notify()' 메서드는 대기 중인 쓰레드를 깨웁니다.

예제 코드:

class SharedResource {
    private boolean available = false;

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

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

public class ThreadCommunicationExample {
    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) {
                e.printStackTrace();
            }
        });

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

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

설명:
- 'wait()'와 'notify()' 메서드를 사용하여 생산자와 소비자 간의 협력을 구현했습니다.

쓰레드 풀

쓰레드 풀(Thread Pool)은 여러 쓰레드를 미리 생성해두고 작업 큐에 들어온 작업을 재사용 가능한 쓰레드에 할당하여 처리하는 방식입니다. 이는 쓰레드를 효율적으로 관리하고, 쓰레드를 반복적으로 생성하는 비용을 줄일 수 있습니다.

예제 코드:

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

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

        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
            });
        }

        executor.shutdown();
    }
}

설명:
- 'ExecutorService'를 사용하여 쓰레드 풀을 생성하고, 3개의 고정된 쓰레드로 작업을 처리합니다.


8. 멀티쓰레딩에서 발생할 수 있는 문제

멀티쓰레딩 환경에서는 다양한 문제가 발생할 수 있으며, 이를 해결하기 위해서는 적절한 동기화와 쓰레드 제어가 필요합니다.

교착 상태 (Deadlock)

교착 상태는 두 개 이상의 쓰레드가 서로 자원을 기다리며 무한히 대기 상태에 빠지는 상황입니다. 이를 피하기 위해 자원의 순서를 정하거나, 타임아웃을 설정할 수 있습니다.

예제 코드:

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

    public void method1() {
        synchronized (lock1) {
            System.out.println("Lock1 acquired, waiting for Lock2");
            synchronized (lock2) {
                System.out.println("Lock1 and Lock2 acquired");
            }


        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Lock2 acquired, waiting for Lock1");
            synchronized (lock1) {
                System.out.println("Lock2 and Lock1 acquired");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        Thread thread1 = new Thread(example::method1);
        Thread thread2 = new Thread(example::method2);

        thread1.start();
        thread2.start();
    }
}

설명:
- 위 예제는 교착 상태가 발생할 수 있는 상황을 보여주며, 두 쓰레드가 서로 상대방의 자원을 기다리며 무한히 대기하게 됩니다.

경쟁 상태 (Race Condition)

경쟁 상태는 여러 쓰레드가 동시에 공유 자원에 접근할 때 발생하는 문제입니다. 이를 해결하기 위해서는 동기화를 통해 자원 접근을 제어해야 합니다.

예제 코드:

class RaceConditionExample {
    private int counter = 0;

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

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        RaceConditionExample example = new RaceConditionExample();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + example.getCounter());
    }
}

설명:
- 동기화를 통해 경쟁 상태를 해결하여 여러 쓰레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지합니다.


9. 예제와 분석

지금까지 배운 쓰레드와 멀티쓰레딩 개념들을 종합적으로 적용한 예제를 살펴보겠습니다.

종합 예제:

class SharedCounter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

코드 분석:
- 동기화를 사용하여 여러 쓰레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지했습니다.
- 'join()' 메서드를 사용하여 모든 쓰레드가 작업을 완료한 후에 최종 결과를 출력합니다.


10. 결론 및 추가 학습 자료

이번 글에서는 자바의 쓰레드와 멀티쓰레딩에 대해 살펴보았습니다. 쓰레드는 프로세스 내에서 실행되는 독립적인 작업 흐름으로, 멀티쓰레딩을 통해 여러 작업을 동시에 처리할 수 있습니다. 자바에서 쓰레드를 생성하고 제어하는 방법, 동기화와 쓰레드 간의 통신 방법을 이해하면, 복잡한 멀티쓰레드 애플리케이션을 효과적으로 개발할 수 있습니다.

추가 학습 자료:
- 자바 공식 문서: [Oracle Java Documentation - Concurrency](https://docs.oracle.com/javase/tutorial/essential/concurrency/)
- 온라인 자바 튜토리얼: [W3Schools Java Threads](https://www.w3schools.com/java/java_threads.asp)
- 자바 코딩 연습 사이트: [GeeksforGeeks - Multithreading in Java](https://www.geeksforgeeks.org/multithreading-in-java/)

쓰레드와 멀티쓰레딩은 자바 프로그래밍의 중요한 부분으로, 성능을 최적화하고 복잡한 작업을 병렬로 처리할 수 있도록 도와줍니다. 이번 기회를 통해 쓰레드의 개념과 활용 방법을 잘 이해하고, 실무에서 효과적으로 사용해보세요.


이제 자바의 쓰레드와 멀티쓰레딩에 대해 자세히 이해하게 되었습니다. 다음 글에서는 자바의 또 다른 고급 기능에 대해 다루도록 하겠습니다. 자바의 더 깊은 이해를 위해 계속해서 학습해나가세요!

반응형

'자바' 카테고리의 다른 글

자바 네트워킹  (0) 2024.08.29
자바 동기화  (2) 2024.08.28
자바 파일 입출력  (0) 2024.08.26
자바 스트림 API  (0) 2024.08.25
자바 람다 표현식  (0) 2024.08.24