Java/Grammar

[Java 문법] 쓰레드 (Thread)

annovation 2024. 11. 27. 08:50

쓰레드 (Thread)

쓰레드(Thread) 프로그램 내에서 실행되는 가장 작은 작업 단위입니다.
자바에서 쓰레드는 하나의 프로세스 내에서 여러 작업을 동시에 수행(병렬 처리)할 수 있도록 지원하는 기능입니다.

  • 프로세스(Process) : 실행 중인 프로그램.
  • 쓰레드(Thread) : 프로세스 내에서 실행되는 작업 단위

특징

 

  1. 멀티스레딩(Multithreading)
    • 하나의 프로세스에서 여러 쓰레드가 동시에 실행되는 방식.
    • 예: 웹 브라우저에서 하나의 쓰레드가 화면을 렌더링하고, 다른 쓰레드가 파일을 다운로드.
  2. 공유 메모리
    • 쓰레드는 같은 프로세스 내에서 메모리를 공유하므로, 서로 데이터를 쉽게 주고받을 수 있음.
    • 하지만 공유 자원을 동시에 접근할 때 동기화 문제가 발생할 수 있음.
  3. 경량성
    • 쓰레드는 프로세스보다 가볍고 생성, 소멸 비용이 적음.
  4. 병렬 처리
    • CPU 코어를 효율적으로 활용하여 동시에 여러 작업을 수행할 수 있음.

주요 메서드와 예제 

 

* 주요 메서드

메서드 설명 사용 빈도
start() 새로운 쓰레드를 생성하고 run() 메서드를 호출하여 실행. ⭐⭐⭐
run() 쓰레드가 실행할 작업을 정의. 일반적으로 start()에 의해 호출됨. ⭐⭐
sleep(long millis) 현재 쓰레드를 지정된 시간 동안 일시 정지 상태로 만듦. ⭐⭐⭐
join() 호출한 쓰레드가 종료될 때까지 현재 쓰레드를 대기 상태로 만듦. ⭐⭐⭐
interrupt() 현재 실행 중인 쓰레드를 인터럽트 상태로 변경. ⭐⭐
isAlive() 쓰레드가 실행 중인지 여부를 반환. ⭐⭐
yield() 현재 쓰레드가 실행 중인 상태를 양보하고 다른 쓰레드에게 실행 기회를 줌.
wait() 호출한 쓰레드를 대기 상태로 전환. 동기화 블록에서만 사용 가능. ⭐⭐⭐
notify() wait() 상태의 쓰레드 중 하나를 깨움. 동기화 블록에서만 사용 가능. ⭐⭐⭐
notifyAll() wait() 상태의 모든 쓰레드를 깨움. ⭐⭐
setPriority(int priority) 쓰레드의 우선순위를 설정 (1~10). 기본값은 5.
getPriority() 쓰레드의 현재 우선순위를 반환.

 

 

* 예제

  • start()와 run()

          start()는 새로운 쓰레드를 생성하고, 내부적으로 run() 메서드를 호출합니다.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("쓰레드 실행 중");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 새로운 쓰레드 생성 및 실행
    }
}

 

  • sleep()

          sleep()은 현재 쓰레드를 일정 시간 동안 멈추게 합니다.

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("쓰레드 시작");
            try {
                Thread.sleep(2000); // 2초 동안 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("쓰레드 종료");
        });

        thread.start();
    }
}

 

➡️ 출력

쓰레드 시작
(2초 대기)
쓰레드 종료

 

 

  • join()

          join()은 호출한 쓰레드가 종료될 때까지 기다립니다.

public class JoinExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("쓰레드 작업 중...");
            try {
                Thread.sleep(1000); // 1초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("쓰레드 작업 완료");
        });

        thread.start();
        try {
            thread.join(); // 쓰레드가 종료될 때까지 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("메인 쓰레드 종료");
    }
}

 

➡️ 출력

쓰레드 작업 중...
쓰레드 작업 완료
메인 쓰레드 종료

 

  • wait()와 notify()

        멀티스레드 간 통신을 구현하는 데 사용합니다.

class SharedResource {
    private boolean ready = false;

    public synchronized void waitForSignal() {
        while (!ready) {
            try {
                wait(); // 다른 쓰레드가 notify() 호출할 때까지 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("신호를 받았습니다!");
    }

    public synchronized void sendSignal() {
        ready = true;
        notify(); // 대기 중인 쓰레드 하나를 깨움
        System.out.println("신호를 보냈습니다!");
    }
}

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

        Thread waitingThread = new Thread(resource::waitForSignal);
        Thread notifyingThread = new Thread(resource::sendSignal);

        waitingThread.start();
        try {
            Thread.sleep(100); // 신호 보내기 전에 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyingThread.start();
    }
}

 

➡️ 출력

신호를 보냈습니다!
신호를 받았습니다!

 


start() run()의 차이 

 

  • start()
    • 새로운 쓰레드를 생성하고, 쓰레드의 run() 메서드를 실행.
    • 병렬로 작업을 수행.
  • run()
    • 현재 실행 중인 쓰레드에서 run() 메서드를 호출.
    • 병렬 작업이 아닌, 일반 메서드 호출처럼 동작.

 

 

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread 실행");
        });

        thread.start(); // 새로운 쓰레드에서 실행
        thread.run();   // 현재 쓰레드(main)에서 실행
    }
}

 

➡️ 출력

Thread 실행 (새로운 쓰레드에서 실행)
Thread 실행 (현재 쓰레드에서 실행)

 


기본 사용법 2가지 

자바에서 쓰레드를 생성하고 실행하는 방법은 크게 두 가지가 있습니다.

 

  1. Thread 클래스 상속
  2. Runnable 인터페이스 구현

 


 

1. Thread 클래스 상속

Thread 클래스를 상속받아 쓰레드를 생성하는 방법입니다.

public class MyThread extends Thread {
    @Override
    public void run() {
        // 쓰레드에서 실행할 작업
        for (int i = 1; i <= 5; i++) {
            System.out.println("Thread: " + i);
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread(); // 쓰레드 객체 생성
        thread.start(); // 쓰레드 실행
    }
}

 

➡️ 출력

Thread: 1
Thread: 2
Thread: 3
Thread: 4
Thread: 5

 


2. Runnable 인터페이스 구현 

Runnable 인터페이스를 구현하여 쓰레드를 생성하는 방법입니다.

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 쓰레드에서 실행할 작업
        for (int i = 1; i <= 5; i++) {
            System.out.println("Runnable: " + i);
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable()); // Runnable 구현체 전달
        thread.start(); // 쓰레드 실행
    }
}

 

➡️ 출력

Runnable: 1
Runnable: 2
Runnable: 3
Runnable: 4
Runnable: 5

 

 


 

쓰레드 공유 객체(Thread Shared Object)

 쓰레드 공유 객체(Thread Shared Object)여러 쓰레드가 동시에 접근하거나 사용하는 객체를 의미합니다.
쓰레드는 같은 프로세스 내에서 메모리를 공유하므로, 특정 객체를 여러 쓰레드가 동시에 사용할 수 있습니다.

공유 객체는 자원의 효율적인 활용을 가능하게 하지만, 동시에 데이터 무결성과 동기화 문제를 발생시킬 수 있습니다.

 

 

* 특징

 

  1. 공유 메모리
    • 자바에서는 쓰레드가 같은 힙 메모리 공간을 공유하므로, 하나의 객체를 여러 쓰레드가 동시에 사용할 수 있습니다.
  2. 문제점
    • 여러 쓰레드가 동시에 객체의 상태를 변경하려고 하면 데이터 충돌이나 일관성 문제가 발생할 수 있습니다.
  3. 해결책
    • 동기화(Synchronization)를 통해 쓰레드가 공유 객체를 안전하게 접근하도록 제어합니다.

 

* 예제

 

다음은 두 쓰레드가 동일한 카운터 객체를 공유하며 값을 증가시키는 예제입니다.

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    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();

        // 결과가 항상 2000이 아닐 수 있음 (데이터 충돌 발생 가능)
        System.out.println("최종 카운트: " + counter.getCount());
    }
}

 

 

➡️ BUT, 데이터 무결성에 문제 발생

 

  • increment() 메서드가 여러 쓰레드에서 동시에 호출될 경우, count 값이 정확히 업데이트되지 않을 수 있습니다.
  • count++는 다음과 같은 세 단계로 이루어지며, 이 과정이 중단되면 문제가 발생합니다:
    1. count 값을 읽음
    2. count 값을 증가
    3. count 값을 저장

 

➡️ 해결 방법 : 쓰레드 동기화 (Synchronization) -> synchronized 키워드를 사용

 

 

*공유 객체를 사용하는 이유

 

  1. 자원 효율성
    • 여러 쓰레드가 동일한 객체를 공유하여 메모리 사용을 최소화
  2. 작업 분담
    • 여러 쓰레드가 하나의 객체를 사용해 병렬로 작업을 처리
  3. 일관된 데이터
    • 공유 객체를 사용하여 쓰레드 간에 데이터 전달 및 동기화

 

 

* 주의점

 

  1. 동기화 필수
    • 공유 객체에 대한 쓰레드의 접근을 적절히 동기화하지 않으면 데이터 무결성 문제가 발생.
  2. 데드락(Deadlock)
    • 잘못된 동기화로 인해 쓰레드가 서로 무한히 기다리는 상태가 발생할 수 있음.
  3. 경합 조건(Race Condition)
    • 두 개 이상의 쓰레드가 동일한 자원에 동시에 접근하여 발생하는 비정상적인 결과.
  4. 성능 저하
    • 지나친 동기화는 성능을 저하시킬 수 있으므로 필요 최소한으로 동기화 적용.

 


동기화의 주요 방법 

 

1. synchronized 키워드

  • 메서드나 블록 단위로 사용하여 공유 객체에 대한 접근을 제어

2. ReentrantLock 사용

  • 더 세밀한 동기화 제어를 위해 java.util.concurrent.locks.ReentrantLock을 사용
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 락 획득
        try {
            count++;
        } finally {
            lock.unlock(); // 락 해제
        }
    }

    public int getCount() {
        return count;
    }
}

 

3. Atomic 클래스

  • java.util.concurrent.atomic 패키지에서 제공하는 AtomicInteger, AtomicLong 등을 사용하면 동기화 없이 원자적 연산 가능
import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.getAndIncrement();
    }

    public int getCount() {
        return count.get();
    }
}

 

 


쓰레드 동기화 (Synchronization) 

여러 쓰레드가 공유 자원에 동시에 접근하면 데이터가 일관성 없게 처리될 수 있습니다. 이를 해결하기 위해 동기화를 사용합니다.

 

  • synchronized 키워드를 사용하여 공유 자원을 보호합니다.
public class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }

    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("최종 카운트: " + counter.getCount());
    }
}

 

➡️ 출력

최종 카운트: 2000

 

 

  • 동기화 블록을 사용하여 increment() 메서드의 특정 부분을 동기화합니다.
public void increment() {
    synchronized (this) { // 특정 부분만 동기화
        count++;
    }
}

 

 


멀티스레드(Multithreading) 

 멀티스레드(Multithreading)는 하나의 프로세스(Process)에서 여러 개의 쓰레드(Thread)를 생성하여 동시에 실행하는 것을 의미합니다. 이를 통해 하나의 프로그램이 여러 작업을 병렬로 처리할 수 있어 CPU 사용률을 최적화하고, 응답성과 처리 속도를 높일 수 있습니다.

 

 

* 장점

 

  1. 성능 향상
    • 여러 작업을 동시에 처리하여 CPU 활용도를 극대화
  2. 응답성 개선
    • 대기 시간이 긴 작업(예: 파일 다운로드)도 병렬로 처리하여 사용자 응답성을 높임
  3. 자원 공유
    • 같은 프로세스 내에서 메모리를 공유하므로 효율적

 

* 단점

 

  • 복잡성 증가
    • 동기화, 경합 조건(Race Condition) 등으로 인해 코드가 복잡해질 수 있음
  • 디버깅 어려움
    • 병렬 처리로 인해 발생하는 문제를 재현하거나 디버깅하기 어려움
  • 자원 소비
    • 지나치게 많은 쓰레드를 생성하면, 문맥 교환(Context Switching) 오버헤드로 성능이 저하될 수 있음

데몬 스레드 (Daemon Thread) 

 데몬 스레드(Daemon Thread) 백그라운드에서 실행되는 보조 쓰레드입니다.
주 쓰레드(Main Thread) 또는 사용자 쓰레드가 실행 중일 때 보조 역할을 수행하며, 모든 사용자 쓰레드가 종료되면 데몬 스레드도 자동으로 종료됩니다.

 

 

* 일반 스레드 (User Thread) VS 데몬 스레드 (Daemon Thread)

특징 일반 쓰레드(User Thread) 데몬 쓰레드(Daemon Thread)
역할 주요 작업 실행 백그라운드에서 보조 작업 수행
프로그램 종료 시 동작 모든 일반 쓰레드가 종료될 때까지 실행 유지 모든 일반 쓰레드가 종료되면 자동으로 종료
설정 기본적으로 일반 쓰레드 setDaemon(true)로 설정해야 함

 

 

* 예시

public class DaemonThreadExample {
    public static void main(String[] args) {
        // 일반 쓰레드
        Thread userThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("사용자 쓰레드 실행: " + i);
                try {
                    Thread.sleep(1000); // 1초 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 데몬 쓰레드
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("데몬 쓰레드 실행 중...");
                try {
                    Thread.sleep(500); // 0.5초 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        daemonThread.setDaemon(true); // 데몬 쓰레드로 설정

        userThread.start();  // 사용자 쓰레드 시작
        daemonThread.start(); // 데몬 쓰레드 시작
    }
}

 

➡️ 출력

데몬 쓰레드 실행 중...
사용자 쓰레드 실행: 1
데몬 쓰레드 실행 중...
데몬 쓰레드 실행 중...
사용자 쓰레드 실행: 2
데몬 쓰레드 실행 중...
데몬 쓰레드 실행 중...
사용자 쓰레드 실행: 3
데몬 쓰레드 실행 중...
데몬 쓰레드 실행 중...
사용자 쓰레드 실행: 4
데몬 쓰레드 실행 중...
데몬 쓰레드 실행 중...
사용자 쓰레드 실행: 5
  • userThread는 5번 출력 후 종료
  • daemonThread는 백그라운드에서 계속 실행되지만, userThread가 종료되면 함께 종료

 

* 주의 사항

 

  1. start() 전에 설정해야 함
    • setDaemon(true)는 쓰레드가 시작되기 전에만 호출 가능
    • 이미 시작된 쓰레드에서는 IllegalThreadStateException 발생
  2. 데몬 쓰레드는 중요한 작업에 사용하지 말 것
    • 사용자 쓰레드가 종료되면 데몬 쓰레드도 종료되므로, 중요한 데이터 처리나 작업에는 부적합

 


결론 

 

 

  • 쓰레드는 자바에서 멀티태스킹을 구현하는 기본 단위로, 하나의 프로세스에서 여러 작업을 동시에 처리할 수 있도록 합니다.
  • 효율적인 작업 분할, 병렬 처리, 응답성 향상을 위해 자주 사용됩니다.
  • 하지만, 공유 자원의 동기화와 같은 문제를 올바르게 처리하지 않으면 오류가 발생할 수 있으므로 신중하게 설계해야 합니다.

 

 


출처

OpenAI ChatGPT (https://openai.com)

 

 

'Java > Grammar' 카테고리의 다른 글

[Java 문법] 람다식 Lamda  (2) 2024.11.29
[Java 문법] 어노테이션 (Annotation)  (2) 2024.11.28
[Java 문법] 래퍼 클래스 (Wrapper Class)  (0) 2024.11.25
[Java 문법] String.split() method  (2) 2024.11.24
[Java 문법] for-each 구문  (0) 2024.11.22