[JAVA] 스레드 (Thread) 정리

2025. 1. 25. 23:33·BackEnd/JAVA

1. 스레드 (Thread)란?

스레드(Thread)는 프로그램 내에서 독립적으로 실행될 수 있는 최소 실행 단위입니다. 자바에서 스레드는 병렬로 여러 작업을 처리하거나 동시에 작업을 실행할 수 있도록 지원합니다. 프로세스 내에서 스레드들은 같은 자원을 공유하면서 실행됩니다.

1.1 프로세스 VS 스레드

  • 프로세스: 운영체제로부터 독립된 자원을 할당받아 실행되는 프로그램의 단위입니다. 각 프로세스는 서로 독립적이며 자원을 공유하지 않습니다.
  • 스레드: 한 프로세스 내에서 여러 작업을 병렬로 실행하는 실행 흐름의 단위입니다. 여러 스레드가 한 프로세스 내에서 자원을 공유하며 작업을 나눠서 처리할 수 있습니다.

2. 자바에서 스레드 사용 방법

자바에서 스레드를 사용하는 방법은 크게 두 가지가 있습니다.

  • Thread 클래스를 상속받는 방법
  • Runnable 인터페이스를 구현하는 방법

2.1 Thread 클래스를 상속받는 방법

Thread 클래스를 상속받아 run() 메서드를 오버라이드하는 방식입니다. 이 방법은 새로운 클래스를 정의하고, 그 안에서 스레드에서 실행될 코드를 작성하는 방식입니다.

예시 코드

class MyThread extends Thread {  // Thread 클래스를 상속받음
    public void run() {          // run() 메서드를 오버라이드하여 스레드에서 실행할 코드를 정의
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread running: " + i); // 스레드가 실행되는 동안 5번 반복
            try {
                Thread.sleep(1000);  // 1초간 스레드를 멈춤 (1000밀리초)
            } catch (InterruptedException e) {
                e.printStackTrace();  // 예외 처리
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread(); // 스레드 객체 생성
        thread.start();                   // 스레드 시작 (run() 메서드 호출)
    }
}

// 출력 결과
/*
Thread running: 0
Thread running: 1
Thread running: 2
Thread running: 3
Thread running: 4
*/
  • thread.start()를 호출하면, 새로운 스레드가 생성되어 run() 메서드가 실행됩니다. Thread.sleep(1000)을 통해 스레드는 1초씩 대기합니다. 이 코드는 5초 동안 스레드가 5번 출력되도록 동작합니다.

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

Runnable 인터페이스를 구현하여 run() 메서드 안에 스레드에서 실행될 코드를 작성합니다. 이 방법은 다중 상속이 불가능한 자바의 특성상, 다른 클래스를 상속받는 경우에 유용합니다.

예시 코드

class MyRunnable implements Runnable {  // Runnable 인터페이스를 구현
    public void run() {                 // run() 메서드에서 스레드가 실행할 코드를 정의
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnable running: " + i);
            try {
                Thread.sleep(1000);  // 1초간 멈춤
            } catch (InterruptedException e) {
                e.printStackTrace();  // 예외 처리
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable()); // Runnable을 스레드로 감쌈
        thread.start();                               // 스레드 시작 (run() 메서드 실행)
    }
}

// 출력 결과
/*
Runnable running: 0
Runnable running: 1
Runnable running: 2
Runnable running: 3
Runnable running: 4
*/
  • _new MyRunnable()_은 Runnable 인터페이스를 구현한 클래스를 생성하고, 이를 Thread 객체로 감싼 후 start() 메서드를 호출하여 실행합니다. *_Thread.sleep(1000)_으로 스레드를 1초씩 멈추면서 5번 출력합니다.

3. 왜 스레드를 사용하는가?

스레드를 사용하는 이유는 크게 병렬 처리와 성능 향상 때문입니다. 여러 작업을 동시에 처리하거나 대기 시간이 긴 작업을 병렬로 처리함으로써 시스템 자원을 효율적으로 사용할 수 있습니다.

3.1 스레드를 사용하는 자세한 이유

1) 병렬 처리

  • 여러 작업을 동시에 실행하여 시스템 자원을 효율적으로 사용할 수 있습니다.
  • 예시: 파일 다운로드와 사용자 인터페이스(UI) 처리를 동시에 수행할 수 있습니다.

2) 성능 향상

  • 멀티코어 CPU 환경에서 여러 스레드를 활용해 병렬 처리로 성능을 극대화

3) 비동기 작업 처리

  • 네트워크 통신, 파일 입출력과 같은 긴 작업을 별도의 스레드에서 처리해 메인 스레드의 블로킹 방지 (메인 스레드가 계속해서 다른 작업을 처리할 수 있음)

4) 서버와 클라이언트 작업 분리

  • 서버에서 각 클라이언트 요청을 별도 스레드로 처리하여 여러 사용자의 요청을 병렬로 처리 및 응답

4. 스레드의 상태

스레드는 실행 중 다양한 상태로 변경됩니다. 자바에서 스레드의 상태는 다음과 같이 구분됩니다.

4.1 스레드의 상태와 전환

1) New

  • 스레드가 생성되었지만 아직 실행되지 않은 상태입니다.
  • start() 메서드가 호출되기 전 상태입니다.

2) Runnable

  • 스레드가 실행 중이거나 실행할 준비가 된 상태 (실행 대기 상태)입니다.
  • CPU에 의해 할당되면 실행됩니다.

3) Blocked

  • 스레드가 동기화 블록에 들어가기 위해 대기 중인 상태입니다.
  • 다른 스레드가 자원을 점유하고 있으면 블로킹 상태가 됩니다.

4) Waiting

  • 스레드가 다른 스레드의 작업이 완료될 때까지 대기하고 있는 상태입니다.
  • Object.wait() 메서드에 의해 대기 상태에 들어갑니다.

5) Timed Waiting

  • 스레드가 일정 시간 동안 기다리고 있는 상태입니다.
  • Thread.sleep()이나 Object.wait(long)에 의해 이 상태로 들어갑니다.

6) Terminated

  • 스레드의 작업이 완료되어 종료된 상태입니다.
  • run() 메서드가 끝나면 스레드는 종료됩니다.

5. 스레드 제어 메서드

자바에서는 스레드를 제어하기 위한 여러 가지 메서드를 제공합니다.

5.1 start()

스레드를 실행시키는 메서드입니다. 이 메서드를 호출하면 스레드가 실행되기 시작하며, run() 메서드가 호출됩니다.

Thread thread = new Thread(new MyRunnable());
thread.start();  // 스레드 시작

5.2 sleep(long millis)

스레드를 일시 정지시켜 주어진 시간 동안 멈추게 합니다. 시간이 지나면 다시 실행됩니다.

Thread.sleep(1000);  // 1초 동안 스레드 정지

5.3 join()

다른 스레드가 종료될 때까지 기다리도록 현재 스레드를 멈춥니다. 즉, 호출된 스레드가 끝날 때까지 현재 스레드는 기다립니다.

thread.join();  // 해당 스레드가 종료될 때까지 현재 스레드 대기

5.4 interrupt()

스레드를 중단시킵니다. sleep() 상태에 있는 스레드를 깨울 때 사용되며, InterruptedException이 발생합니다.

thread.interrupt();  // 스레드 중단 요청

5.5 isAlive()

스레드가 실행 중인지 확인합니다. 스레드가 TERMINATED 상태에 있으면 false를 반환합니다.

boolean alive = thread.isAlive();  // 스레드가 아직 실행 중인지 확인

6. 스레드 활용 예시

6.1 여러 스레드 실행 및 제어

스레드를 사용하여 동시에 여러 작업을 병렬로 처리하는 방법을 예시로 보여줍니다. 두 개의 스레드를 실행하고, 각 스레드가 작업을 완료할 때까지 기다리는 방식입니다.

예시 코드

class MyRunnable implements Runnable {
    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // 스레드에서 실행될 코드: 1초마다 메시지를 출력하는 작업을 3번 반복합니다.
        for (int i = 0; i < 3; i++) {
            System.out.println(name + " running: " + i);
            try {
                Thread.sleep(1000);  // 1초간 멈춤
            } catch (InterruptedException e) {
                // 스레드가 중단될 경우 예외 처리
                System.out.println(name + " interrupted!");
            }
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 두 개의 스레드를 생성하여 실행
        Thread thread1 = new Thread(new MyRunnable("Thread 1"));
        Thread thread2 = new Thread(new MyRunnable("Thread 2"));

        // 스레드 실행
        thread1.start();  // 스레드 1 시작
        thread2.start();  // 스레드 2 시작

        // 메인 스레드는 thread1과 thread2가 모두 끝날 때까지 대기
        thread1.join();   // 스레드 1이 끝날 때까지 기다림
        thread2.join();   // 스레드 2가 끝날 때까지 기다림

        System.out.println("Both threads finished.");  // 모든 스레드가 종료된 후 실행
    }
}

// 출력 결과
/*
Thread 1 running: 0
Thread 2 running: 0
Thread 1 running: 1
Thread 2 running: 1
Thread 1 running: 2
Thread 2 running: 2
Both threads finished.
*/
  • 두 개의 스레드가 각각 1초 간격으로 "running" 메시지를 3번 출력합니다.
  • thread1.start()와 thread2.start()*를 통해 두 개의 스레드가 동시에 실행됩니다.
  • join() 메서드는 해당 스레드가 완료될 때까지 메인 스레드가 대기하게 만듭니다. 따라서 두 스레드가 모두 종료된 후 "Both threads finished." 메시지가 출력됩니다.
  • Thread.sleep(1000)으로 각 스레드는 1초 간격으로 실행되며, 출력 순서는 OS 스케줄러에 의해 달라질 수 있습니다. 이 예시에서는 스레드가 동시에 실행되므로 출력 순서가 교차되어 나타날 수 있습니다.

6.2 스레드 간 동기화

멀티스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근하면 데이터 불일치가 발생할 수 있습니다. 이를 해결하기 위해 동기화 (synchronization)가 필요합니다.

자바는 synchronized 키워드와 volatile 키워드를 제공하여 스레드 간의 자원 접근을 제어합니다.

1) 동기화 없이 자원 접근 시 문제점

동기화가 적용되지 않으면 여러 스레드가 동시에 자원에 접근하여 작업 결과가 예상과 다르게 나올 수 있습니다.

문제 상황 코드

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();  // t1이 끝날 때까지 대기
        t2.join();  // t2가 끝날 때까지 대기

        // 예상 출력은 2000이지만, 실제로는 그보다 작은 값이 출력될 수 있음
        System.out.println("Final count without synchronization: " + counter.getCount());
    }
}

// 동기화 없이 실행하면, 출력 결과는 예기치 않게 작을 수 있습니다.
// 출력 예시 (동기화 없을 경우, 실행할 때마다 결과가 달라질 수 있음)
/*
Final count without synchronization: 1850
*/
  • increment() 메서드는 스레드 간에 동기화되지 않아 동시에 실행될 경우 데이터 경쟁(Race Condition) 문제가 발생합니다.
  • 예를 들어, 두 스레드가 동일한 시점에 count++를 실행하면 동시에 동일한 값을 읽고 증가하게 되어 최종 결과가 예상치보다 낮을 수 있습니다.

2) 해결책: synchronized로 동기화

synchronized 키워드를 사용하여 자원에 대한 접근을 한 번에 하나의 스레드만 허용함으로써 데이터 불일치 문제를 방지할 수 있습니다.

동기화 된 코드

class Counter {
    private int count = 0;

    // synchronized 키워드를 사용하여 동기화된 카운터 증가
    public synchronized 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();  // t1이 끝날 때까지 대기
        t2.join();  // t2가 끝날 때까지 대기

        // 동기화 덕분에 항상 2000이 출력됨
        System.out.println("Final count with synchronization: " + counter.getCount());
    }
}

// 출력 결과 (동기화된 경우):
/*
Final count with synchronization: 2000
*/
  • synchronized 메서드: 메서드 전체를 동기화하여 여러 스레드가 동시에 호출할 수 없도록 보장합니다.
  • 한 스레드가 increment() 메서드를 실행하는 동안 다른 스레드는 해당 메서드에 접근할 수 없습니다.
  • 이를 통해 카운터 값이 정확히 증가하도록 보장합니다.

3) 효율적 동기화: synchronized 블록 사용

메서드 전체를 동기화하면 불필요하게 많은 코드가 동기화 대상이 되어 성능 저하를 유발할 수 있습니다. 따라서 필요한 코드 부분만 synchronized 블록으로 동기화하는 것이 더 효율적입니다.

동기화 블록 코드

class Counter {
    private int count = 0;
    private int anotherCount = 0; // 두 번째 카운터 변수

    // synchronized 블록을 사용하여 필요한 부분만 동기화
    public void incrementBoth() {
        synchronized (this) {
            count++;  // 이 블록은 한 번에 하나의 스레드만 접근 가능
            anotherCount++;  // 두 변수 모두 안전하게 동기화됨
        }
    }

    public int getCount() {
        return count;
    }

    public int getAnotherCount() {
        return anotherCount;
    }
}

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.incrementBoth();  // 두 변수를 동시에 증가시킴
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementBoth();  // 두 변수를 동시에 증가시킴
            }
        });

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

        t1.join();  // t1이 끝날 때까지 대기
        t2.join();  // t2가 끝날 때까지 대기

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

// 출력 결과:
/*
Final count: 2000
Final anotherCount: 2000
*/
  • synchronized(this) 블록 내에서는 count와 anotherCount 두 변수를 모두 증가시키고 있습니다. 이 블록은 동시에 하나의 스레드만 접근할 수 있으므로, 두 변수 모두 안전하게 동기화되어 정확하게 2000으로 증가합니다.
  • 만약 이 동기화 블록이 없었다면, 두 스레드가 동시에 두 변수를 증가시키는 작업을 할 수 있기 때문에 결과값이 예측할 수 없게 될 수 있습니다.
  • 이 방법은 메서드 전체를 동기화하지 않고 필요한 코드 부분만 동기화하여 효율성을 높입니다.

4) 경량 동기화: volatile 키워드 사용

동기화 대신 volatile 키워드를 사용하여 변수의 일관성을 보장할 수 있습니다. volatile은 변수 값을 스레드 간에 공유하고, 캐싱 문제를 방지하지만, 동기화 블록처럼 동시 실행 제어는 제공하지 않습니다.

class SharedData {
    private volatile boolean running = true;  // volatile로 변수 선언

    public void stop() {
        running = false;  // 다른 스레드에서 값을 바꾸면 모든 스레드에 즉시 반영됨
    }

    public boolean isRunning() {
        return running;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SharedData sharedData = new SharedData();

        Thread worker = new Thread(() -> {
            while (sharedData.isRunning()) {  // running이 true일 동안 루프
                System.out.println("Worker is running");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Worker stopped");
        });

        worker.start();  // 스레드 시작

        Thread.sleep(2000);  // 2초 대기 후

        sharedData.stop();  // worker 스레드를 중지시킴
        worker.join();      // worker 스레드가 끝날 때까지 기다림
    }
}

// 출력 결과:
/*
Worker is running
Worker is running
Worker is running
Worker is running
Worker stopped
*/
  • volatile로 선언된 변수는 항상 메인 메모리에서 읽고 쓰기 때문에, 한 스레드가 값을 변경하면 다른 스레드에서 즉시 반영됩니다. (여러 스레드에 걸쳐 즉시 반영되며, 캐싱을 방지)
  • 하지만, volatile은 변수의 일관성만 보장하며, synchronized처럼 자원의 동시 접근을 제어하지는 못합니다.
  • 이 코드는 sharedData.isRunning() 값을 반복적으로 체크하면서 루프를 제어합니다. volatile을 사용하지 않으면 한 스레드가 변수 값을 변경해도 다른 스레드에서는 이전 값을 캐시하여 일관성이 깨질 수 있습니다.

5) 동기화 방법 비교

동기화 방식 설명 적용 사례
synchronized 메서드 메서드 전체를 동기화 여러 스레드가 자원을 공유하며 복잡한 연산을 수행할 때
synchronized 블록 특정 코드 블록만 동기화 메서드 전체가 아닌 일부 코드만 동기화가 필요할 때
volatile 키워드 변수의 일관성만 보장 (경량) 단순 플래그 변수나 값 변경을 즉시 반영해야 할 때, 복잡한 연산(예: 증가 연산) 대신 간단한 읽기/쓰기 작업에 사용

7. 정리

  • 스레드(Thread): 프로그램 내에서 독립적으로 실행될 수 있는 최소 실행 단위로, 병렬 처리와 성능 최적화를 위해 사용됩니다.
  • 생성 방법: Thread 클래스 상속 또는 Runnable 인터페이스 구현
  • 스레드 상태: New, Runnable, Blocked, Waiting, Timed Waiting, Terminated로 구분
  • 스레드 동기화: synchronized 키워드로 자원 충돌 방지, volatile로 변수 일관성 보장
  • 스레드 제어 메서드: start(), sleep(), join(), interrupt(), isAlive() 등이 주요 메서드
  • 멀티스레드 주의사항: 데이터 불일치를 방지하기 위해 동기화 사용 및 병렬 처리 효율성 고려

'BackEnd > JAVA' 카테고리의 다른 글

[JAVA] 와일드카드 (Generic Wildcards) 정리  (2) 2025.02.05
[JAVA] 제네릭 (Generic) 정리  (1) 2025.01.26
[JAVA] 인터페이스 심화: 다중 상속, 다중 구현  (4) 2025.01.20
[JAVA] try-with-resources 정리  (2) 2025.01.19
[JAVA] 자바 부모클래스 및 인터페이스 심화: 오버라이딩과 메서드 동작  (1) 2025.01.17
'BackEnd/JAVA' 카테고리의 다른 글
  • [JAVA] 와일드카드 (Generic Wildcards) 정리
  • [JAVA] 제네릭 (Generic) 정리
  • [JAVA] 인터페이스 심화: 다중 상속, 다중 구현
  • [JAVA] try-with-resources 정리
개발자 동긔
개발자 동긔
배우고 느낀점들을 기록합니다. 열정 넘치는 백엔드 개발자로 남고싶습니다.
  • 개발자 동긔
    Donker Dev
    개발자 동긔
  • 전체
    오늘
    어제
    • Category (39)
      • BackEnd (23)
        • JAVA (15)
        • Spring & JPA (7)
      • Database (4)
      • Computer Science (2)
        • Network (0)
        • Security (0)
        • Web (1)
      • DevOps (6)
        • Docker (1)
        • Jenkins (0)
        • Monitoring (2)
        • CICD (1)
      • 트러블 슈팅 (3)
      • 성능 개선 (1)
      • Project (0)
  • 인기 글

  • 태그

    JPA
    restful api
    interface
    restful api 설계
    @RequestBody
    nginx
    Jenkins
    SSH
    docker
    인터페이스
    master/slave db 이중화 처리
    Database
    java
    spring boot
    mysql master/slave replication
    docker compose
    와일드카드
    Spring
    CICD
    spring cloud msa
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
개발자 동긔
[JAVA] 스레드 (Thread) 정리
상단으로

티스토리툴바