ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java - Thread
    Language/Java 2022. 3. 28. 14:19

    메인 스레드

    메인 스레드의 동작
    public static void main(String[] args) {
        String data = null;
    
        if (...) {
    
        }
    
        while (...) {
                
        }
    }

    Java 애플리케이션에서 메인 스레드는 main() 메서드가 실행될 때 시작되며, main() 메서드에서 마지막 코드를 실행하거나 return 문을 만나게 되면 종료된다.

     

     

    메인 스레드는 필요에 따라 작업 스레드를 만들어서 위 사진과 같이 병렬로 코드를 실행할 수 있다.

     

    싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료되지만, 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.

     

    특히, 메인 스레드가 작업 스레드보다 먼저 종료되어도 작업 스레드가 실행 중이라면 프로세스는 종료되지 않는 점을 주의해야 한다.


    작업 스레드의 생성과 실행

    작업 스레드의 생성 방법

     

    Java에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다. 스레드를 생성하기 위해서는 크게 Thread 클래스를 직접 객체화하는 방법과 스레드를 상속한 클래스를 생성하는 방법이 있다.

     

    Thread 클래스에서 직접 생성
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
        
        }
    });

    Thread 클래스로부터 작업 스레드 객체를 직접 생성하기 위해서는 위 코드와 같이 Runnable 객체를 매개 값으로 갖는 생성사를 호출하면 된다.

     

    Thread thread = new Thread(() -> System.out.println("작업 스레드입니다."));

    Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체이다. 이런 Runnable은 인터페이스이기 때문에 구현 클래스를 정의해 주어야 한다. run() 메서드를 재정의해주면 되고, 위 코드처럼 람다식으로 간단하게 표현이 가능하다.

     

    작업 스레드는 생성되자마자 바로 실행되지는 않고, 별도로 start() 메서드를 호출해야 한다. start() 메서드가 호출되면 작업 스레드가 매개 값으로 받은 Runnable의 run() 메서드를 호출하여 작업을 처리한다.

     

    스레드에 대해서 어느 정도 이해하였으니, 예제를 살펴보면서 좀 더 자세히 알아보겠다. 아래의 예제는 0.5초 주기로 비프음을 발생시키면서 동시에 문구를 출력시키는 코드이다.

     

    1. 메인 스레드만 사용한 예제

    public class Main {
        public static void main(String[] args) {
            Toolkit toolkit = Toolkit.getDefaultToolkit();
    
            for (int i = 0; i < 5; i++) {
                toolkit.beep();   // 비프음 발생
    
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            for (int i = 0; i < 5; i++) {
                System.out.println("띵");
    
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    메인 스레드만 존재할 경우 비프음을 내는 작업이 다 끝나야만 출력 작업이 가능하다. 실제로 위 코드를 실행하면 비프음이 5번 발생한 이우에 출력 작업이 시작되는 것을 알 수 있다.

     

    2. 메인 스레드와 작업 스레드를 동시에 사용한 예제

    public class Main {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
    
                for (int i = 0; i < 5; i++) {
                    toolkit.beep();
    
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
    
            for (int i = 0; i < 5; i++) {
                System.out.println("띵");
    
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    메인 스레드에서 비프음을 방생하던 일을 작업 스레드에게 위임하였다. thread.start() 메서드를 호출되면서 새로운 작업 스레드가 비프음을 발생하는 작업을 시작하고, 메인 스레드는 비프음을 출력하는 작업을 시작한다.

     

    Thread 하위 클래스에서 작성
    class BeepThread extends Thread {
    
        @Override
        public void run() {
            Toolkit toolkit = Toolkit.getDefaultToolkit();
    
            for (int i = 0; i < 5; i++) {
                toolkit.beep();
    
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Thread thread = new BeepThread();
            thread.start();
    
            for (int i = 0; i < 5; i++) {
                System.out.println("띵");
    
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    Thread 클래스를 상속받아 run() 메서드를 오버라이딩하는 방식이다. 람다식을 사용한 예제와 거의 유사하지만, Thread 클래스를 상속하여 만든 하위 클래스로 작업 스레드를 구현하면 run() 메서드 외에 개발자가 원하는 메서드를 더 정의할 수 있다는 장점이 있다.


    스레드의 이름

    스레드의 이름

    스레드의 이름은 기본적으로 Thread-n 형식으로 붙여진다. n은 0부터 시작하며, 원한다면 개발자가 임의의 이름을 정해줄 수 있다. 스레드의 이름은 모르지만 현재 코드를 실행하는 스레드가 누구인지 궁금할 수 있다. 이러한 경우, Thread의 정적 메서드인 currentTread() 메서드를 호출하면 된다.

     

    public class Main {
        public static void main(String[] args) {
            Thread mainThread = Thread.currentThread();
            System.out.println("프로그램 시작 스레드 이름 : " + mainThread.getName());
    
            Thread threadA = new ThreadA();
            System.out.println("작업 스레드 이름 : " + threadA.getName());
            threadA.start();
    
            Thread threadB = new ThreadB();
            System.out.println("작업 스레드 이름 : " + threadB.getName());
            threadB.start();
        }
    }
    
    class ThreadA extends Thread {
        public ThreadA() {
            setName("ThreadA");
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 2; i++) {
                System.out.println(getName() + "이(가) 출력한 내용");
            }
        }
    }
    
    class ThreadB extends Thread {
    
        @Override
        public void run() {
            for (int i = 0; i < 2; i++) {
                System.out.println(getName() + "이(가) 출력한 내용");
            }
        }
    }

    ThreadA는 이름을 정해주었고, ThreadB는 기본 명명 규칙을 따라간다. 위 코드를 실행하면 아래와 같은 결과가 나온다.

     

    프로그램 시작 스레드 이름 : main
    작업 스레드 이름 : ThreadA
    작업 스레드 이름 : Thread-1
    ThreadA이(가) 출력한 내용
    ThreadA이(가) 출력한 내용
    Thread-1이(가) 출력한 내용
    Thread-1이(가) 출력한 내용

    출력 순서는 다를 수 있다.


    스레드 우선순위

    동시성(Concurrency) vs 병렬성(Parallelism)

    동시성과 병렬성은 두 단어 모두 동시에 무언가를 하는 것이 아닌가 생각하기 쉽다. 하지만, 정확한 의미는 아래와 같다.

     

     

     

    동시성

     - 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질이다.

     - CPU 하나가 Time Sharing 기법을 통해 실제로 동시에 스레드가 실행되는 것은 아니지만, CPU 제어권을 매우 빠르게 스레드에게 줬다가 뺏으면서 사람이 보기에 마치 동시에 실행되는 것처럼 보이는 것을 뜻한다.

     

    병렬성

     - 멀티 작업을 위해 멀치 코어에서 개별 스레드를 할당받아 동시에 실행하는 것을 말한다.

     - 말 그대로 각자의 CPU가 나누어서 각자의 일을 하여 실질적인 동시 작업을 수행하는 것을 뜻한다.

     

    스레드 스케줄링

    스레드의 개수가 코어의 수보다 많을 경우 어떤 스레드에게 CPU 제어권을 주어야 하는지 결정해야 하는데, 이를 스레드 스케줄링이라고 한다. 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 자신의 run() 메서드를 조금씩 실행한다.

    Java의 스레드 스케줄링은 주로 우선순위 방식과 라운드 로빈 방식을 사용한다. 전자의 방식은 프로그래머가 특정 스레드의 우선순위를 코드로 제어할 수 있지만, 후자의 경우 JVM에 의해 정해 지므로 코드로 제어할 수 없다. 따라서, 이번 시간에는 우선순위 방식의 스레드 스케줄링만 이야기해 보겠다.

     

    우선순위 방식
    public class Main {
        public static void main(String[] args) {
            for (int i = 1; i <= 10; i++) {
                Thread thread = new CalcThread("thread-" + i);
    
                if (i != 10) {
                    thread.setPriority(Thread.MIN_PRIORITY);
                } else {
                    thread.setPriority(Thread.MAX_PRIORITY);
                }
                thread.start();
            }
        }
    }
    
    class CalcThread extends Thread {
        public CalcThread(String name) {
            setName(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 2_000_000_000; i++) {
    
            }
            System.out.println(getName());
        }
    }

    우선순위는 1에서부터 10까지 주어지는데, 숫자가 클수록 우선순위가 높다. Thread의 setPriority() 메서드를 각 스레드의 우선순위를 설정하고, 우선순위에 따른 실행 순서를 확인하기 위한 코드를 작성하였다.

     

    thread-10
    thread-1
    thread-2
    thread-3
    thread-6
    thread-5
    thread-9
    thread-4
    thread-8
    thread-7

    위 코드를 실행하면 위와 같은 결과를 확인할 수 있다. thread-10이 우선순위가 가장 높기 때문에 먼저 수행되고, 나머지는 우선순위가 같으므로 정확한 순서를 판단할 수 없다. 실제로 매 실행마다 결과가 다르게 나오는 것을 확인할 수 있을 것이다.


    동기화 메서드와 동기화 블록

    공유 객체를 사용할 때의 주의할 점
    public class Main {
        public static void main(String[] args) {
            Calculator calculator = new Calculator();
    
            User01 user01 = new User01();
            user01.setCalculator(calculator);
            user01.start();
    
            User02 user02 = new User02();
            user02.setCalculator(calculator);
            user02.start();
        }
    }
    
    class Calculator {
        private int memory;
    
        public int getMemory() {
            return this.memory;
        }
    
        public void setMemory(int memory) {
            this.memory = memory;
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread().getName() + " : " + this.memory);
        }
    }
    
    class User01 extends Thread {
        private Calculator calculator;
    
        public void setCalculator(Calculator calculator) {
            this.setName("User01");
            this.calculator = calculator;
        }
    
        @Override
        public void run() {
            calculator.setMemory(100);
        }
    }
    
    class User02 extends Thread {
        private Calculator calculator;
    
        public void setCalculator(Calculator calculator) {
            this.setName("User02");
            this.calculator = calculator;
        }
    
        @Override
        public void run() {
            calculator.setMemory(50);
        }
    }

    멀티 스레드 환경에서 객체를 공유해서 사용하면 문제가 발생할 수 있다. 위 코드를 통해 문제점을 알아보겠다.

     

    User01 : 50
    User02 : 50

    위 코드의 결과가 의도한 바와는 다르게 출력되는 것을 확인할 수 있다.

     

    동기화 메서드 및 동기화 블록

    1. 임계 영역(Critical Section)

    멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역이라고 한다. Java에서는 이러한 임계 영역을 지정하기 위해 동기화(synchronized) 메서드와 동기화 블록을 제공한다. 스레드가 객체 내부의 동기화 메서드 또는 블록에 들어가면 즉시 해당 객체에 lock을 걸어 다른 스레드가 임계 영역 코드를 실행하지 못하도록 한다.

     

    2. 동기화 메서드

    public synchronized void method() {
        임계 영역
    }

    동기화 메서드는 메서드 어디든 붙일 수 있으며, 스레드가 동기화 메서드를 실행하는 즉시 객체에 lock이 걸리고 동기화 메서드 실행을 종료해야 lock이 풀린다.

     

    3. 동기화 블록

    public void method() {
    
        // 여러 스레드가 실행 가능한 영역
        
        synchronized(공유 객체) {
            임계 영역
        }
        
        // 여러 스레드가 실행 가능한 영역
    }

    일부 내용만 임계 영역을 만들고 싶다면 위와 같이 동기화 블록을 만들 수 있다.

     

    공유 객체의 동기화 처리

    1. 동기화 메서드를 이용한 처리

    class Calculator {
        private int memory;
    
        public int getMemory() {
            return this.memory;
        }
    
        public synchronized void setMemory(int memory) {
            this.memory = memory;
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread().getName() + " : " + this.memory);
        }
    }

    공유 자원을 사용하는 setMemory() 메서드에 동기화 처리를 해주면 된다.

     

    2. 동기화 블록을 이용한 처리

    class Calculator {
        private int memory;
    
        public int getMemory() {
            return this.memory;
        }
    
        public void setMemory(int memory) {
            synchronized (this) {
                this.memory = memory;
    
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println(Thread.currentThread().getName() + " : " + this.memory);
    
            }
        }
    }

    동기화 블록을 사용하면, setMemory() 메서드를 실행할 수는 있지만 동기화 블록에 한 스레드가 진입하는 순간 동기화 블록의 매개 변수에 해당하는 객체에 lock을 건다.


    스레드 상태

    스레드 상태의 종류

    getState() 메서드를 사용해서 스레드의 상태를 확인할 수 있다. 위 그림과 같이 스레드 객체가 만들어진 후 실행 대기 상태에 있다가 실행도 하고 일시 정지 상태가 되다가 최종적으로는 종료 상태가 된다.

     

    스레드 상태에 따른 열거 상수 정리

    1. 객체 생성

     - NEW : 스레드 객체가 생성되었고, 아직 start() 메서드가 호출되지 않은 상태

     

    2. 실행 대기

     - RUNNABLE : 실행 상태로 언제든지 갈 수 있는 상태

     

    3. 일시 정지

     - WAITING : 다른 스레드가 통지할 때까지 가다리는 상태

     - TIMED WAITING : 주어진 시간 동안 기다리는 상태

     - BLOCKED : 사용하고자 하는 객체의 lock이 풀릴 때까지 기다리는 상태

     

    4. 종료

     - TERMINATED : 실행을 끝마친 상태

     

    스레드 상태를 확인하는 예제
    public class Main {
        public static void main(String[] args) {
            StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
            statePrintThread.start();
        }
    }
    
    class StatePrintThread extends Thread {
        private Thread targetThread;
    
        public StatePrintThread(Thread targetThread) {
            this.targetThread = targetThread;
        }
    
        @Override
        public void run() {
            while (true) {
                Thread.State state = targetThread.getState();
    
                System.out.println("타켓 스레드 상태 : " + state);
    
                /** 스레드 생성 */
                if (state == State.NEW) {
                    targetThread.start();
                }
    
                /** 스레드 종료 */
                if (state == State.TERMINATED) {
                    break;
                }
    
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    class TargetThread extends Thread {
    
        @Override
        public void run() {
            for (long i = 0; i < 1_000_000_000; i++) {
    
            }
    
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            for (long i = 0; i < 1_000_000_000; i++) {
    
            }
        }
    }

    1. StatePrintThread

     - 0.3초마다 현재 스레드의 상태를 출력한다.

     - 스레드가 NEW 상태면 RUNNABLE 상태로 만들어주고, 스레드가 TERMINATED 상태면 무한 루프를 종료한다.

     

    2. TargetThread

     - 10번의 루프를 돌고, 1초 동안 스레드를 TIMED WAITING 상태에 빠지게 하고, 다시 10억 번의 루프를 돈다.

     

    3. 출력 결과

    타켓 스레드 상태 : NEW
    타켓 스레드 상태 : RUNNABLE
    타켓 스레드 상태 : TIMED_WAITING
    타켓 스레드 상태 : TIMED_WAITING
    타켓 스레드 상태 : TIMED_WAITING
    타켓 스레드 상태 : RUNNABLE
    타켓 스레드 상태 : TERMINATED

    스레드의 상태 제어

    스레드 상태 제어 메서드

     

    앞서서 스레드의 상태를 단순하게 알아보았고, 이번에는 스레드의 상태를 제어할 수 있는 메서드에 대해서 알아보겠다.

     

     

    위 표에서 wait(), notify(), notifyAll()는 Object 클래스의 메서드이고, 나머지는 Thread 클래스의 메서드이다.

     

    sleep() - 주어진 시간 동안 일시 정지
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // interrupt() 메서드가 호출되면 실행
    }

    매개변수로 들어온 시간만큼 스레드를 일시 정지 상태로 만들고, 시간이 다 지나면 실행 대기 상태로 변경한다. 일시 정지 시간이 다 지나기 전에 스레드를 실행 대기 상태로 만들고 싶다면 interrupt() 메서드를 외부에서 호출하면 된다.

     

    yield() - 다른 스레드에게 실행 양보

    yield() 메서드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 더 높은 우선순위를 갖는 다른 스레드가 실행할 기회를 가질 수 있도록 한다.

     

    public class Main {
        public static void main(String[] args) {
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();
    
            threadA.start();
            threadB.start();
    
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.work = false;
    
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.work = true;
    
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.stop = true;
            threadB.stop = true;
        }
    }
    
    class ThreadA extends Thread {
        public boolean stop = false;
        public boolean work = true;
    
        @Override
        public void run() {
            while (!stop) {
                if (work) {
                    System.out.println("ThreadA 작업 내용");
                } else {
                    Thread.yield();
                }
            }
            System.out.println("ThreadA 작업 종료");
        }
    }
    
    class ThreadB extends Thread {
        public boolean stop = false;
        public boolean work = true;
    
        @Override
        public void run() {
            while (!stop) {
                if (work) {
                    System.out.println("ThreadB 작업 내용");
                } else {
                    Thread.yield();
                }
            }
            System.out.println("ThreadB 작업 종료");
        }
    }

    ThreadA와 ThreadB 모두 각각 플래그를 가지고 있고, 이 플래그에 따라서 출력 내용이 추가되거나 무한 루프를 탈출한다. run() 메서드를 보면 work가 false일 때 yield() 메서드를 호출하여 다른 스레드에게 CPU 제어권을 넘기는 것을 확인할 수 있다.

    따라서, main() 메서드 초반에는 두 스레드가 번갈아가면서 CPU를 얻다가 ThreadA의 work가 false가 되면 yield() 메서드를 호출하여 ThreadB가 CPU 제어권을 많이 얻도록 한다. 이런 식으로 무의미하게 CPU를 잡고 시간을 보내는 일을 줄일 수 있다.

     

     join() - 다른 스레드의 종료를 기다림

    A 스레드가 B 스레드의 일이 종료할 때까지 기다렸다가 일을 수행해야 할 수 있다. 이러한 경우에 사용하는 메서드가 join() 메서드이다.

     

    public class Main {
        public static void main(String[] args) {
            SumThread sumThread = new SumThread();
            sumThread.start();
    
            try {
                sumThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("1부터 100까지의 합 : " + sumThread.getSum());
        }
    }
    
    
    class SumThread extends Thread {
        private long sum;
    
        public long getSum() {
            return sum;
        }
    
        public void setSum(long sum) {
            this.sum = sum;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
        }
    }

    main() 메서드 내에서 sumThread.join()을 통해 sumThread의 작업이 끝날 때까지 메인 스레드는 대기한다. 만약, 해당 메서드가 없었다면 바로 출력문을 실행하기 때문에 1에서 100까지의 합이 0으로 출력된다.

     

    wait(), notify(), notifyAll() - 스레드 간 협업

    정확히 교대 작업이 필요한 상황이 있다. 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.

    이러한 방법은 공유 객체를 사용하는데, 먼저 공유 객체에 대해 두 스레드가 작업할 내용을 각각 동기화 메서드 또는 동기화 블록 처리를 한다. 그리고 한 스레드가 작업을 완료하면 notify() 메서드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고 자신은 wait() 메서드를 호출하여 일시 정지 상태로 만든다.

     

     

    만약 wait() 메서드 대신 wait(long timeout)와 같이 매개 변수가 있는 메서드를 사용하면 notify() 메서드 없이도 주어진 시간이 지나면 자동으로 스레드가 실행 대기 상태가 된다. 참고로 notify()는 wait()에 의해 일시 정지된 스레드 중 하나를 실행 대기 상태로 만들고, notifyAll()은 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만든다.

    위에서 언급한 3가지 메서드는 반드시 동기화 메서드 또는 동기화 블록 내에서 사용해야 한다.

     

    예제01

    public class Main {
        public static void main(String[] args) {
            WorkObject workObject = new WorkObject();
    
            ThreadA threadA = new ThreadA(workObject);
            ThreadB threadB = new ThreadB(workObject);
    
            threadA.start();
            threadB.start();
        }
    }
    
    class WorkObject {
        public synchronized void methodA() {
            System.out.println("ThreadA의 methodA() 작업 실행");
            notify();   // 일시 정지 상태에 있는 ThreadB를 실행 대기 상태로 변경
    
            try {
                wait();   // ThreadA를 일시 정지 상태로 변경
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public synchronized void methodB() {
            System.out.println("ThreadB의 methodB() 작업 실행");
            notify();   // 일시 정지 상태에 있는 ThreadA를 실행 대기 상태로 변경
    
            try {
                wait();   // ThreadA를 일시 정지 상태로 변경
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    class ThreadA extends Thread {
    
        private WorkObject workObject;
    
        public ThreadA(WorkObject workObject) {
            this.workObject = workObject;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                workObject.methodA();
            }
        }
    }
    
    class ThreadB extends Thread {
    
        private WorkObject workObject;
    
        public ThreadB(WorkObject workObject) {
            this.workObject = workObject;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                workObject.methodB();
            }
        }
    }

    서로의 스레드를 일시 정지 상태로 만들었다가 실행 대기 상태로 만드는 과정을 반복하는 코드이다.

     

    ThreadA의 methodA() 작업 실행
    ThreadB의 methodB() 작업 실행
    ThreadA의 methodA() 작업 실행
    ThreadB의 methodB() 작업 실행
    ThreadA의 methodA() 작업 실행
    ThreadB의 methodB() 작업 실행

    위 코드를 실행시키면 위와 같은 결과를 얻는 것을 확인할 수 있다.

     

    예제02

    public class Main {
        public static void main(String[] args) {
            DataBox dataBox = new DataBox();
    
            ProducerThread producerThread = new ProducerThread(dataBox);
            ConsumerThread consumerThread = new ConsumerThread(dataBox);
    
            producerThread.start();
            consumerThread.start();
        }
    }
    
    class DataBox {
        private String data;
    
        /** 소비자 스레드의 임계 영역 */
        public synchronized String getData() {
            if (this.data == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            String returnValue = data;
            System.out.println("ConsumerThread가 읽은 데이터 : " + returnValue);
            data = null;
            notify();
            return returnValue;
        }
    
        /** 생산자 스레드의 임계 영역 */
        public synchronized void setData(String data) {
            if (this.data != null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            this.data = data;
            System.out.println("ProducerThread가 생산한 데이터 : " + data);
            notify();
        }
    }
    
    class ProducerThread extends Thread {
        private DataBox dataBox;
    
        public ProducerThread(DataBox dataBox) {
            this.dataBox = dataBox;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                String data = "Data-" + i;
                dataBox.setData(data);
            }
        }
    }
    
    class ConsumerThread extends Thread {
        private DataBox dataBox;
    
        public ConsumerThread(DataBox dataBox) {
            this.dataBox = dataBox;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                String data = dataBox.getData();
            }
        }
    }

    위 코드는 데이터를 저장하는 생산자 스레드와 데이터를 소비하는 소비자 스레드가 각자의 역할을 교대로 수행하는 코드이다. 생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성해서는 안되고, 소비자 스레드는 생산자 스레드가 새로운 데이터를 생성하기 전에 데이터를 두 번 데이터를 두 번 읽어서는 안 된다.

     

    stop 플래그, interrupt() - 스레드의 안전한 종료

    스레드를 종료하기 위한 가장 쉬운 방법은 stop() 메서드를 사용하는 것이다. 하지만, stop() 메서드는 사용 중이었던 자원을 회수하지 않고 종료해 버린다는 단점으로 인해 현재는 deprecated 된 상태이기 때문에 이 메서드의 사용은 지양해야 한다. 그 대신 아래에서 두 가지 대체 방법을 알아보겠다.

     

    1. stop 플래그를 사용하는 방법

    public class XXXThread extends Thread {
    
        private boolean stop;
    
        @Override
        public void run() {
            while (!stop) {
               // 스레드가 반복 실행하는 코드
            }
           // 스레드가 사용한 자원 정리
        }
    }

    스레드 클래스 내에 플래그를 하나 두고, 해당 플래그의 값에 따라서 스레드의 실행이 수행되도록 결정하는 방법이다.

     

    2. interrupt() 메서드를 이용하는 방법

    interrupt() 메서드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이를 이용하여 프로그래머는 catch 블록에서 스레드를 정상 종료시키거나, Thread.interrupted() 메서드를 통해 현재 스레드가 interrupt 요청을 받았는지 확인하여 스레드를 종료시킬 수 있다.

     

    public class Main {
        public static void main(String[] args) {
            Thread thread = new PrintThread();
            thread.start();
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            thread.interrupt();
        }
    }
    
    class PrintThread extends Thread {
    
        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println("실행 중");
                    Thread.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("자원 정리");
            System.out.println("실행 종료");
        }
    }

    위 코드는 InterruptedExeption 예외를 이용해서 스레드를 종료시키는 예제이다.

    주의할 점은 스레드가 실행 대기 상태이거나 실행 중인 상태라면 interrupt 요청이 들어와도 스레드가 종료되지 않는다. 대신 interrupt 요청이 들어온 상태에서 일시 정지 상태가 되는 즉시 InterriptedExeption 예외가 발생하도록 의도적으로 run() 메서드 내부에 Thread.sleep(1) 코드를 작성한 것이다.

     

    public class Main {
        public static void main(String[] args) {
            Thread thread = new PrintThread();
            thread.start();
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            thread.interrupt();
        }
    }
    
    class PrintThread extends Thread {
    
        @Override
        public void run() {
            while (true) {
                System.out.println("실행 중");
    
                if (Thread.interrupted()) {
                    break;
                }
            }
    
            System.out.println("자원 정리");
            System.out.println("실행 종료");
        }
    }

    위 코드는 Thread.interrupted() 메서드를 사용하여 스레드를 종료시키는 예제이다.

    현재 스레드에 대해 interrupted() 메서드가 실행되었는지 확인하고, 실행된 상태라면 true를 반환하여 무한 루프를 탈출하는 로직이다.


    데몬 스레드

    데몬 스레드는 주 스레드의 작업을 돕는 보조 스레드로, 스레드가 종료되면 데몬 스레드도 같이 종료된다는 특징이 있다. 데몬 스레드의 적용 예로는 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 가비지 컬렉터 등이 있다. 이 기능들은 주 스레드인 워드 프로세서, 미디어 플레이어, JVM이 종료되면 같이 종료된다.

    스레드를 데몬 스레드로 만들려면 setDaemon() 메서드의 매개 변수를 true로 하여 실행하면 된다.

     

    public class Main {
        public static void main(String[] args) {
            AutoSaveThread autoSaveThread = new AutoSaveThread();
            autoSaveThread.setDaemon(true);
            autoSaveThread.start();
    
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("메인 스레드 종료");
        }
    }
    
    class AutoSaveThread extends Thread {
        public void save() {
            System.out.println("작업 내용을 저장");
        }
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break;
                }
                save();
            }
        }
    }

    AutoSaveThread는 메인 스레드의 데몬 스레드가 되어 보조 역할을 수행한다. AutoSaveThread는 1초 주기로 작업 내용을 저장하다가 메인 스레드가 종료되면 같이 종료된다.


    스레드 그룹

    스레드 그룹이란?

    스레드 그룹은 관련된 스레드를 하나로 묶어서 관리할 목적으로 이용된다. JVM이 실행되면 system 스레드 그룹을 만들고, JVM 운영에 필요한 스레드를 생성하여 system 스레드 그룹에 포함한다. 그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main 스레드 그룹에 포함한다.

    스레드는 반드시 하나의 스레드 그룹에 포함되며, 스레드 그룹을 명시적으로 정해주지 않으면 자신을 생성한 스레드와 같은 스레드 그룹에 속하게 된다. 프로그래머가 생성하는 작업 스레드는 대부분 메인 스레드가 생성하므로 main 스레드 그룹에 속한다.

     

    스레드 그룹 이름 얻기
    public class Main {
        public static void main(String[] args) {
            AutoSaveThread autoSaveThread = new AutoSaveThread();
            autoSaveThread.setName("AutoSaveThread");
            autoSaveThread.setDaemon(true);
            autoSaveThread.start();
    
            Map<Thread, StackTraceElement[]> threadInfos = Thread.getAllStackTraces();
    
            threadInfos.keySet().forEach(thread -> {
                System.out.println("Name : " + thread.getName() + ((thread.isDaemon()) ? "(데몬)" : "(주)"));
                System.out.println("소속 그룹 : " + thread.getThreadGroup().getName());
                System.out.println();
            });
        }
    }

    Thread.currentThread().getThreadGroup()을 이용해서 특정 스레드의 그룹 이름을 가져오거나 Thread.getAllStackTraces()를 이용해서 현재 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻어와서 그룹 이름을 얻을 수 있다.

     

    Name : Attach Listener(데몬)
    소속 그룹 : system
    
    Name : Finalizer(데몬)
    소속 그룹 : system
    
    Name : Monitor Ctrl-Break(데몬)
    소속 그룹 : main
    
    Name : AutoSaveThread(데몬)
    소속 그룹 : main
    
    Name : main(주)
    소속 그룹 : main
    
    Name : Signal Dispatcher(데몬)
    소속 그룹 : system
    
    Name : Reference Handler(데몬)
    소속 그룹 : system

    코드를 실행시키면 위와 같은 결과를 얻을 수 있다.

     

    스레드 그룹 생성

    스레드 그룹을 만드는 방법은 2가지가 존재한다.

     

    1. 부모 스레드 그룹을 명시하지 않음

    ThreadGroup tg = new ThreadGroup(String name);

    위의 경우, 현재 스레드가 속한 그룹의 하위 그룹으로 생성된다.

     

    2. 부모 스레드 그룹을 명시함

    ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);

    매개 변수로 넘겨준 parent에 해당하는 스레드가 속한 그룹의 하위 그룹으로 생성된다.

     

    Thread thread = new Thread(ThreadGroup group, Runnable target);
    Thread thread = new Thread(ThreadGroup group, Runnable target, String name);
    Thread thread = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
    Thread thread = new Thread(ThreadGroup group, String name);

    앞선 2가지 중에 한 가지 방법으로 스레드 그룹을 생성하면, 위 4가지 생성자 중 하나를 선택해서 해당 그룹에 스레드를 포함시킬 수 있다.

    Runnable 타입의 target은 Runnable의 구현 객체, stackSize는 JVM이 스레드에 할당할 stack의 크기이다.

     

    스레드 그룹의 일괄 interrupt()
    public class Main {
        public static void main(String[] args) {
            ThreadGroup myGroup = new ThreadGroup("myGroup");
            WorkThread workThreadA = new WorkThread(myGroup, "workThreadA");
            WorkThread workThreadB = new WorkThread(myGroup, "workThreadB");
    
            workThreadA.start();
            workThreadB.start();
    
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("myGroup 스레드 그룹의 interrupt() 메서드 호출");
            myGroup.interrupt();
        }
    }
    
    class WorkThread extends Thread {
    
        public WorkThread(ThreadGroup threadGroup, String threadName) {
            super(threadGroup, threadName);
        }
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println(getName() + "interrupted");
                    break;
                }
            }
            System.out.println(getName() + " 종료되었습니다.");
        }
    }

     

    스레드 그룹의 interrupt() 메서드를 호출하면 해당 그룹에 속한 모든 스레드에 interrupt 요청을 보낼 수 있다.

     

    myGroup 스레드 그룹의 interrupt() 메서드 호출
    workThreadBinterrupted
    workThreadB 종료되었습니다.
    workThreadAinterrupted
    workThreadA 종료되었습니다.

    myGroup 스레드 그룹의 스레드에 대해 모두 interrupt 요청을 보내서 스레드들을 종료하였다.


    출처

    https://steady-coding.tistory.com/543

     

    728x90

    댓글

Designed by Tistory.