[Java] Thread

Java

Language :

프로세스 Process

실행 중인 프로그램(Program).

프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원, 쓰레드로 구성되어 있다.

최소 하나 이상의 쓰레드가 존재한다.

싱글쓰레드 프로세스 (Single-Threaded Process)

하나의 쓰레드를 가진 프로세스.

두 개 이상의 작업을 처리할 때 한 작업을 마친 후에 다른 작업을 시작한다.

싱글 코어는 단순히 CPU만을 사용하는 계산 작업에 효율적이다.

public class Test {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 500; i++) {
            System.out.printf("-");
        }
        System.out.printf("Time : " + (System.currentTimeMillis() - start));

        for (int i = 0; i < 500; i++) {
            System.out.printf("|");
        }
        System.out.printf("Time2 : " + (System.currentTimeMillis() - start));
    }
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------Time : 67||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||Time2 : 78

    멀티쓰레드 프로세스 (Multi-Threaded Process)

    둘 이상의 쓰레드를 가진 프로세스.

    두 개 이상의 작업을 처리할 때 번갈아 가면서 작업을 수행한다.

    이 때 쓰레드간의 작업 전환에 시간이 걸리기 때문에 싱글쓰레드 보다 작업 시간이 더 걸릴 수 있다.

    쓰레드들이 서로 다른 자원을 사용해서 작업할 때 효율적 → 한 쓰레드가 작업 중이면 다른 쓰레드는 대기한다.

    • 싱글 코어

         병행(Concurrent). 작업이 절대 겹치지 않는다.

    • 멀티 코어

         병렬(Parallel). 쓰레드가 동시에 수행될 수 있어 작업이 겹치는 부분이 발생한다. 

         화면(Console)이라는 자원을 놓고 두 쓰레드가 경쟁하게 된다.

    public class Test { // (Multi-Core, Multi-Thread)
        static long startTime = 0;
    
        public static void main(String[] args) {
    
            ThreadEx t = new ThreadEx();
            t.start();
    
            long start = System.currentTimeMillis();
    
            for (int i = 0; i < 500; i++) {
                System.out.printf("-");
            }
            System.out.printf("tiem2 : " + (System.currentTimeMillis() - start));
        }
    }
    
    class ThreadEx extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 500; i++) {
                System.out.printf("|");
            }
            System.out.printf("tiem : " + (System.currentTimeMillis() - Test.startTime));
        }
    }
    ----------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||tiem2 : 93tiem : 1518921785720
    • 불확실성

         OS의 프로세스 스케줄러의 영향을 받아 매 순간 상황에 따라 프로세스에게 할당되는 실행시간이 일정하지 않다.

    • 용어

          - 작업 전환(Context Switching) :  현재 진행 중인 작업의 상태(다음에 실행해야할 위치(PC, 프로그램 카운터) 등의 정보)를 저장하고 읽는 것.

          - 대기 시간 :  한 쓰레드가 화면에 출력하고 있는 동안 다른 쓰레드는 출력이 끝나기를 기다린다.

    쓰레드 Thread

    프로세스의 자원을 이용해서 실제로 작업을 수행하는 것.

    독립적인 작업을 수행하기 위해 개별적인 메모리 공간(호출스택)을 필요로 한다. 

     → 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.

    실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

    • 불확실성

         JVM의 쓰레드 스케줄러에 의해 어떤 쓰레드가 얼마동안 실행될 것인지 결정되기 때문에 쓰레드에게 할당되는 시간이 일정하지 않다.

         JVM의 종류에 따라 쓰레드 스케줄러의 구현 방법이 다를 수 있다.

    멀티쓰레딩 Multi-Threading

    하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것.

    • 장점

         CPU의 사용률을 향상시킨다.

         자원을 보다 효율적으로 사용할 수 있다.

         사용자에 대한 응답성이 향상된다.

         작업이 분리되어 코드가 간결해진다.

    • 단점

         동기화(Synchronization) : 자원을 공유하면서 작업하기 때문에 서로의 작업에 영향을 줄 수 있다.

         교착 상태(Deadlock) : 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태.

    구현 1.   Thread 클래스를 상속 받는다.

    다른 클래스를 상속받을 수 없게 되기 때문에 잘 쓰이지 않는다.

    public class Test extends Thread {
        @Override
        public void run() {
            super.run();
        }
    }
    import java.util.logging.Logger;
    
    public class Test extends Thread {
        Logger logger = Logger.getLogger(Test.class.getName());
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                logger.info("Thread Name : " + getName()); // 쓰레드의 이름을 반환
            } // 쓰레드의 이름을 지정하지 않으면 'Thread-번호'의 형식으로 이름이 정해진다.
        }
    
        public static void main(String[] args) {
    
            Test test = new Test();
            test.start();
        }
    }
    2월 18, 2018 11:52:22 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0
    2월 18, 2018 11:52:23 오전 blog.Test run
    정보: Thread Name : Thread-0

    구현 2.   Runnable 인터페이스를 구현한다.

    run()만 정의되어 있는 간단한 인터페이스로, 일반적으로 쓰이는 방법이다.

    재사용성(Reusability)이 높고, 코드의 일관성(Consistency)을 유지할 수 있다.

    public interface Runnable {
        public abstract void run();
    }

    쓰레드를 사용할 때, 구현한 클래스의 인스턴스를 생성한 후, 그 인스턴스를 Thread 클래스 생성자의 매개변수로 제공해야 한다.

    Thread 클래스의 메서드를 사용할 경우, Thread 클래스의 static 메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어와야 한다.

    import java.util.logging.Logger;
    
    public class Test implements Runnable {
        Logger logger = Logger.getLogger(Test.class.getName());
    
        public static void main(String[] args) {
            Runnable r = new Test();
            Thread t = new Thread(r);
            t.start();
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                logger.info("Thread Name : " + Thread.currentThread().getName());
            }
        }
    }
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    2월 18, 2018 11:57:10 오전 blog.Test run
    정보: Thread Name : Thread-1
    

    start()

    쓰레드를 실행한다.

    실행 대기중인 쓰레드가 하나라도 있으면 실행 대기 상태에 있다가 자신의 차례가 되어야 실행된다.

    새로운 쓰레드가 작업을 실행하는 데 필요한 호출 스택을 생성한 후, run()을 호출하여 생성된 호출 스택에 첫번째로 올라가게 한다.

    한번 실행이 종료된 쓰레드는 다시 실행할 수 없다.

    Thread t = new Thread(new Test());
    t.start();
    // t.start();      error
    
    t = new Thread(new Test());
    t.start();
    • run()

         생성된 쓰레드를 실행시키는 것이라 착각할 수 있는데, 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

    • Main Thread

         main 메서드의 작업을 수행하는 것.

         프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되도록 한다.

    데몬 쓰레드 Daemon Thread

    일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드.

    무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

    일반 쓰레드가 모두 종료되면 강제적으로 자동 종료된다.

    데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.

    • 작성과 실행

         일반 쓰레드와 동일하게 생성한 후 실행하기 전에 setDaemon(true)를 호출하면 된다.

    public final void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 변경
    public final boolean isDaemon()         // 쓰레드가 데몬 쓰레드인지 확인. 데몬 쓰레드면 true 반환
    
    Thread t = new Thread();
    t.setDaemon(true);
    t.start();

         setDaemon 메서드는 바드시 start()를 호출하기 전에 실행되어야 한다.

         그렇지 않으면 IllegalThreadStateException이 발생한다.

    • Example
    import java.util.Iterator;
    import java.util.Map;
    import java.util.logging.Logger;
    
    public class Test {
        public static final Logger logger = Logger.getLogger(Test.class.getName());
    
        public static void main(String[] args) {
    
            ThreadEx1 t1 = new ThreadEx1("Tread1");
            ThreadEx2 t2 = new ThreadEx2("Tread2");
            t1.start();
            t2.start();
        }
    }
    
    class ThreadEx1 extends Thread {
    
        ThreadEx1(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            try {
                sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    class ThreadEx2 extends Thread {
        ThreadEx2(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            Map map = getAllStackTraces();
            Iterator it = map.keySet().iterator();
    
            int x = 0;
            while (it.hasNext()) {
                Object obj = it.next();
                Thread t = (Thread) obj;
                StackTraceElement[] ste = (StackTraceElement[]) (map.get(obj));
    
                Test.logger.info("[" + ++x + "] name : " + t.getName() + ", group: " + t.getThreadGroup().getName() + ", daemon: " + t.isDaemon());
    
                for (int i = 0; i < ste.length; i++) {
                    Test.logger.info(ste[i] + "");
                }
    
                Test.logger.info(" ");
            }
        }
    }
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [1] name : Tread1, group: main, daemon: false
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Thread.sleep(Native Method)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: blog.ThreadEx1.run(Test.java:28)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [2] name : Signal Dispatcher, group: system, daemon: true
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [3] name : Tread2, group: main, daemon: false
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Thread.dumpThreads(Native Method)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Thread.getAllStackTraces(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: blog.ThreadEx2.run(Test.java:42)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [4] name : Finalizer, group: system, daemon: true
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Object.wait(Native Method)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.ref.ReferenceQueue.remove(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.ref.ReferenceQueue.remove(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [5] name : Reference Handler, group: system, daemon: true
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Object.wait(Native Method)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.Object.wait(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.ref.Reference.tryHandlePending(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [6] name : DestroyJavaVM, group: main, daemon: false
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보: [7] name : Attach Listener, group: system, daemon: true
    2월 18, 2018 12:01:13 오후 blog.ThreadEx2 run
    정보:  

    getAllStackTraces()를 이용하면 실행 중 또는 대기 상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.

    JVM은 프로그램 실행시 가비지 컬렉션, 이벤트 처리, 그래픽 처리와 같이 

    프로그램이 실행되는 데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다. 

    이들은 System Thread 또는 Main Thread Group에 속한다.

    참고 서적: 자바의 정석 3판 2

    민갤

    Back-End Developer

    백엔드 개발자입니다.