ABOUT ME

작은 디테일에 집착하는 개발자

Today
-
Yesterday
-
Total
-
  • [Java] 스레드 (Thread)
    IT Study/컴퓨터 기초 2023. 4. 8. 17:47
    728x90

    1. 스레드란?

    컴퓨터의 프로그램이 동시 실행될 수 있도록 하는 것을 멀티 스레딩이라고 합니다.

    스레드는 실행되는 프로그램에서 개별적인 흐름을 만든 것으로, 프로세스 안에서 실행되는 하나의 실행 흐름입니다.

    여러 개의 스레드는 하나의 프로세스에서 동시에 실행될 수 있으며, 서로 공유되는 자원을 이용할 수 있습니다.

     

    1-1. 스레드와 프로세스

    구분 스레드 프로세스
    정의 프로세스 안에서 실행되는 실행 흐름 중 하나 실행 중인 프로그램
    자원 공유 한 프로세스 내 스레드는 메모리 등의 자원 공유 독립적으로 OS에서 자원 할당 받음
    종료 프로세스 내 타 스레드가 종료될 때까지 대기 종료 시, OS에서 자원 회수
    실행 흐름 (순차적) 한 프로세스 내 스레드는
    서로 다른 실행 경로를 가지나, 실행 시간 중첩
    독립적
    전환 스레드 간 전환 시, 적은 리소스 필요 프로세스 간 전환 시, 많은 리소스 필요
    예외 처리 한 프로세스 종료 시, 타 프로세스에 영향 X 스레드 중 하나에 예외 발생 시, 프로세스까지 영향

     

    1-2. 멀티 스레드

    멀티 스레딩은 하나의 프로세스 내에서 여러 개의 스레드가 동시에 실행되는 것을 의미합니다.

    멀티 스레딩을 통해, 다중 작업을 처리하여 시스템 자원을 효율적으로 사용할 수 있으며 처리량과 응답이 높고 빠릅니다.

    그러나 멀티 스레딩을 사용할 때에는 *동기화, *교착상태와 같은 문제를 고려해야 합니다.

    *동기화 : synchronization, 진행 중인 작업을 타 스레드가 간섭하지 않도록 만든 상태

    *교착상태 : deadlock, 두 스레드가 자원을 점유한 상태로 서로 상태가 점유한 자원을 사용하려고 대기하여 진행이 멈춘 상태

     

    1-3. 스레드의 특징

    아래와 같은 스레드의 특징을 고려해 스레드를 사용하면, 프로그램의 성능과 응답성을 향상할 수 있습니다.

    경량 프로세스 스레드는 스택 영역을 제외하고 나머지 메모리 영역을 공유하기 때문에, 데이터 공유가 간단하고 빠릅니다.
    동시성 동시 실행은 아니지만, CPU 코어 수에 따라 번갈아가며 실행되어 여러 작업이 동시 수행되는 것처럼 보입니다.
    비동기성 스레드가 실행되는 동안 타 작업을 수행할 수 있습니다.
    공유 자원 한 프로세스 안에서 실행되는 여러 실행 경로를 가지기 때문에 자원 공유에 주의해야 합니다.

     

    2. 스레드 정의

    2-1. Thread 클래스

    Thread 클래스를 상속받아 (extends Thread) run() 메서드를 오버라이딩하여 스레드를 생성할 수 있습니다.

    이후 메인 클래스에서 생성한 스레드를 불러와 객체명.start()로 실행합니다.

    class ThreadEx extends Thread {
        @Override
        public void run() {
            // 스레드로 실행할 코드 작성
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ThreadEx thread = new ThreadEx();
            thread.start();  // 스레드 실행
        }
    }

     

    2-2. Runnable 인터페이스

    Runnable 인터페이스 내의 run() 메서드를 구현하여 스레드를 생성할 수 있습니다.

    class RunnableEx implements Runnable {
        @Override
        public void run() {
            // 스레드로 실행할 코드 작성
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            RunnableEx runnable = new RunnableEx();
            Thread thread = new Thread(runnable);
            thread.start();  // 스레드 실행
        }
    }

     

    2-3. Thread의 익명 클래스

    익명 클래스(이름이 없는 클래스)를 통해 클래스의 정의와 객체 생성을 동시에 할 수 있습니다.

    익명 클래스를 통해 정의 시, 코드는 간결해지나 사용이 남발되는 경우 가독성이 떨어질 수 있으니 적절히 사용해야 합니다.

    public class Main {
        public static void main(String[] args) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    // 스레드로 실행할 코드 작성
                }
            };
            
            thread.start();  // 스레드 실행
        }
    }

     

    3. 스레드의 우선순위

    스레드의 우선순위는 단순히 스레드 스케줄러에게 우선순위를 알려주는 것으로,

    스케줄러가 스레드의 우선순위를 참고하여 스레드를 실행합니다.

    따라서 우선순위가 높은 스레드가 항상 먼저 실행되는 것은 아닙니다.

     

    그러나 스레드의 우선순위를 높게 설정하면 높은 확률로 해당 스레드가 먼저 실행될 수 있습니다.

    스레드의 우선순위는 1~10의 범위를 가지며, 10과 같은 높은 수가 높은 우선순위를 의미합니다.

    (기본적으로 스레드는 5라는 동일한 우선순위를 가지기 때문에, 실행할 때마다 결과 값이 달라집니다.)

     

    3-1. 우선순위의 지정 : setPriority()

    스레드의 우선순위를 지정하기 위해서는 setPriority() 메서드를 사용합니다.

    Thread threadMax = new Thread();
    Thread threadMid = new Thread();
    Thread threadMin = new Thread();
    
    threadMax.setPriority(Thread.MAX_PRIORITY);    // 10
    threadMid.setPriority(Thread.NORM_PRIORITY);   // 5
    threadMin.setPriority(Thread.MIN_PRIORITY);    // 1

     

    3-2. 우선순위의 지정 - 활용

    public class Main {
        public static void main(String[] args) {
            Thread threadMin = new Thread(new Task(), "Thread-Min");
            Thread threadMid = new Thread(new Task(), "Thread-Mid");
            Thread threadMax = new Thread(new Task(), "Thread-Max");
    
            threadMin.setPriority(Thread.MIN_PRIORITY);
            threadMid.setPriority(Thread.NORM_PRIORITY);
            threadMax.setPriority(Thread.MAX_PRIORITY);
    
            threadMin.start();
            threadMid.start();
            threadMax.start();
        }
        
        // Runnable 인터페이스의 run() 메서드 구현 및 스레드 생성
        static class Task implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.printf("[%s] Priority = %d\n",
                                      Thread.currentThread().getName(),
                                      Thread.currentThread().getPriority());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

     

    위 예제에서는 Runnable 인터페이스의 run() 메서드를 구현하여 스레드를 생성한 Task 클래스를 정의하였습니다.

    Task 클래스에서는 간단히 (구동되고 있을) 현재 스레드의 이름과 우선순위를 출력하고,

    500ms의 딜레이를 갖도록 작성하였습니다.

     

    <실행 결과>
    [Thread-Min] Priority = 1
    [Thread-Mid] Priority = 5

    [Thread-Max] Priority = 10
    [Thread-Min] Priority = 1
    [Thread-Mid] Priority = 5
    [Thread-Max] Priority = 10
    [Thread-Min] Priority = 1
    [Thread-Max] Priority = 10
    [Thread-Mid] Priority = 5
    [Thread-Min] Priority = 1
    [Thread-Mid] Priority = 5
    [Thread-Max] Priority = 10
    [Thread-Mid] Priority = 5
    [Thread-Max] Priority = 10
    [Thread-Min] Priority = 1

     

    역시 예상했던 실행 결과와 달리 우선순위가 가장 낮은 Thread-Min 스레드가 Thread-Max 스레드보다

    먼저 실행되는 것을 확인할 수 있습니다. 따라서 가능한 스레드 우선순위에 의존하지 않도록 코드를 작성해야 합니다.

     

    4. 스레드의 동기화(Synchronization)

    여러 스레드가 공유 자원에 동시 접근 시, 예상치 못한 결과가 발생하거나 디버깅이 발생하는 등의 문제가 발생합니다.

    이러한 문제 해결을 위해 여러 스레드가 공유 자원에 동시 접근하더라도,

    순서에 따라 접근을 제어하여 데이터의 무결성을 보장하는 동기화 기술이 사용됩니다.

     

    4-1. synchronized 키워드를 이용한 동기화

    synchronized 키워드를 통해 메서드나 블록을 통제 영역으로 설정해,

    해당 영역에서는 한 스레드만 실행되도록 설정할 수 있습니다.

    class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public synchronized void decrement() {
            count--;
        }
    
        public synchronized int getCount() {
            return count;
        }
    }

     

    위 코드는 count라는 변수를 선언하고,

    count를 증가/감소시키거나 가져오는 메서드를 synchronized 키워드를 이용해 동기화하고 있습니다.

    이는 해당 메서드가 한 번에 하나의 스레드만 실행할 수 있도록 보장합니다.

     

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
    
            Runnable incrementTask = () -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increment();
                }
            };
    
            Runnable decrementTask = () -> {
                for (int i = 0; i < 10000; i++) {
                    counter.decrement();
                }
            };
    
            Thread thread1 = new Thread(incrementTask);
            Thread thread2 = new Thread(decrementTask);
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
        }
    }

     

    thread1과 thread2는 Counter 객체의 increment, decrement 메서드를 각 호출하고

    이를 통해 서로 다른 스레드가 count 변수를 동시에 조작할 수 있습니다.

    그러나 해당 메서드가 동기화되었기 때문에 하나의 스레드가 해당 메서드를 호출하면

    타 스레드는 해당 메서드의 호출이 완료될 때까지 기다려야 합니다.

     

    ∴ 스레드 동기화는 다중 스레드 환경에서 데이터의 무결성 충돌을 방지하여 안정적인 프로그램 실행을 보장할 수 있습니다.

     

    5. 스레드 풀

    스레드 풀이란, 미리 스레드를 만들어두고 작업이 들어오면 해당 스레드를 할당하여 작업을 처리하는 방식입니다. 

    스레드 풀을 통해 오버헤드를 줄이고, 생성하고 삭제할 때마다 드는 비용을 줄일 수 있습니다.

     

    5-1. 스레드 풀의 생성

    스레드 풀은 ThreadPoolExecutor 클래스를 사용하여 생성할 수 있습니다.

    newFixedThreadPool() 메서드를 통해 고정된 스레드 개수를 가지는 스레드 풀을 생성할 수 있습니다.

     

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolExample {
        public static void main(String[] args) {
            int nThreads = 3;  // 스레드 풀의 크기
            int taskCount = 5; // 처리할 작업 수
            
            // 스레드 풀 생성
            ExecutorService executor = Executors.newFixedThreadPool(nThread);
    
            for (int i = 1; i <= taskCount; i++) {
                final int task = i;
                
                // 작업(스레드) 생성
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        int sum = 0;
                        for (int j = 1; j <= task; j++) { // 1부터 작업 번호까지의 합계
                            sum += j;
                        }
                        System.out.println("Task " + task + " result : " + sum);
                    }
                });
            }
    
            executor.shutdown(); // 스레드 풀 종료
            
            try {
                // 모든 작업이 완료될 때까지 대기
                if (!executor.awaitTermination(500, TimeUnit.MILLISECONDS)) {
                    // 모든 작업이 완료되지 않은 경우, 강제 종료
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                // 스레드 풀 종료 중에 인터럽트 발생한 경우
                executor.shutdownNow();
            }
        }
    }

     

    6. 스레드의 메서드

    아래의 두 예시를 통해 스레드의 메서드를 확인하고 아래의 설명을 읽어보세요.

    // 스레드 생성
    class ThreadEx extends Thread {
        private int count;
    
        public ThreadEx(String name, int count) {
            super(name);
            this.count = count;
        }
    
        @Override
        public void run() {
            System.out.println("Thread " + getName() + " started.");
    
            for (int i = 1; i <= count; i++) {
                System.out.println(getName() + ": " + i); // 현재 스레드 이름과 i 값 출력
    
                try {
                    Thread.sleep(1000); 			// 1초 스레드 일시 정지
                } catch (InterruptedException e) {
                    System.out.println(getName() + " interrupted.");
                    return;
                }
            }
    
            System.out.println("Thread " + getName() + " finished.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            // ThreadEx 객체 생성 및 시작
        	ThreadEx thread1 = new ThreadEx("Thread 1", 3);
            thread1.start();
    
            // ThreadEx 객체 생성 및 시작
            ThreadEx thread2 = new ThreadEx("Thread 2", 5);
            thread2.start();
    
            // main 스레드는 thread1 스레드가 종료될 때까지 대기
            try {
                thread1.join();
            } catch (InterruptedException e) {
                System.out.println("Main thread interrupted.");
                return;
            }
    
            // main 스레드는 thread2 스레드가 종료될 때까지 대기
            try {
                thread2.join();
            } catch (InterruptedException e) {
                System.out.println("Main thread interrupted.");
                return;
            }
    
            System.out.println("All threads finished.");
        }
    }
    <출력 결과 > 
    Thread 1 started.
    Thread 2 started.
    Thread 2: 1
    Thread 1: 1
    Thread 2: 2
    Thread 1: 2
    Thread 2: 3
    Thread 1: 3
    Thread 2: 4
    Thread 1 finished.
    Thread 2: 5
    Thread 2 finished.
    All threads finished.

     

    6-1. start() : 스레드의 실행

    스레드를 생성 이후, start()를 통해 스레드를 실행시킬 수 있습니다. start() 호출과 동시에 run() 메서드가 실행됩니다.

    그러나 해당 스레드가 바로 실행되는 것이 아닌 실행대기 상태로 전환되어 자신의 차례에 실행될 수 있습니다.

    더불어, 1회 실행이 종료된 스레드는 다시 실행할 수 없습니다. 그래서 만약 스레드 작업을 다시 수행하기 위해서는

    새로운 스레드를 생성한 뒤 start()를 호출해야 합니다.

     

    6-2. join() : 스레드의 실행 대기

    join()을 통해 현재 실행 중인 스레드(A)는 대기하게 됩니다. (InterruptedException 예외 처리 필수)

    .join()의 .(점) 앞에 붙은 스레드(B)가 즉시 실행되며, A는 B가 종료될 때까지 대기합니다.

    ▶ 늘 사용하던 main 메서드의 작업을 수행하는 것도 스레드입니다.

    따라서 join을 통해 현재 실행 중이던 main 스레드를 대기시키고 새로운 스레드가 실행될 수 있습니다.

     

    6-3. sleep() : 스레드의 일시 중지

    sleep()을 통해 현재 실행 중인 스레드를 지정한 시간 동안 일시 중지합니다. (InterruptedException 예외 처리 필수)

    일시 중지된 스레드는 지정된 시간이 경과하거나 (아래 나올) interrupt() 메서드가 호출될 때까지 실행을 중지합니다.

     

    6-4. interrupt() : 스레드의 작업 중단

    interrupt()는 스레드의 작업을 강제 중단하기 위해 호출됩니다.

    만약 join(), sleep()을 통해 일시정지 상태인 스레드라면, 해당 스레드를 깨워 실행대기 상태로 만듭니다.

     

    6-5. yield() : 스레드의 양보

    yield()는 현재 실행 중인 스레드를 일시 중단하여 실행대기 상태가 되고, 다른 스레드에 실행 기회를 양보합니다. 

     

    6-6. setName(String name), getName() : 스레드의 이름 지정과 이름 반환

     

    6-7. currentThread() : 현재 실행 중인 스레드를 반환

     

    이번 글을 통해 스레드에 대해 서칭하며

    스레드라는 것은 동시에 여러 작업을 처리할 수 있는 비동기적인 실행 흐름이기 때문에,

    작업 처리 순서가 보장되지 않고 운영체제/JVM에 따라 달라진다는 것을 알았습니다.

     

    '왜? 내가 생각한 순서대로 나오지 않는거야? 짜증나!' 라고 생각하기보다

    스레드 간의 동기화 처리를 적절하게 수행하여 코드를 잘 작성하는 데에 집중해야겠다고 깨닫게 되었습니다.

    (다들 비슷하게 느끼시길 바라며... 이만 물러나도록 하겠습니다. 바이💙)

Designed by Tistory.