Skip to content

Latest commit

 

History

History
209 lines (188 loc) · 16.3 KB

21. MultiThread.md

File metadata and controls

209 lines (188 loc) · 16.3 KB

1. 멀티스레드

1. 스레드란?

  1. 운영체제에서는 프로그램 하나를 처리하는 데 프로세스라는 단위로 처리하게 된다.
  2. 프로그램을 동시에 여러 개를 사용할 때는 프로세스가 각각의 프로그램에 하나 씩 배정되어 멀티 프로세스로 처리한다. 이러한 기능을 멀티 태스킹이라고 부른다.
  3. 멀티 태스킹이 꼭 멀티 프로세스만을 의미하는 것은 아니다. 만약 크롬에서 하나의 탭에는 유튜브나 넷플릭스의 영상을 재생하고 다른 탭에서는 웹서핑을 진행하면 하나의 프로그램이 동시에 두 작업을 진행하게 되는데 하나의 프로세스만 할당이 되고 두 작업을 동시에 진행가능하게 해주는 개념이 멀티 스레드.
  4. 프로그램의 멀티태스킹은 멀티 프로세스. 프로그램 내부에 멀티태스킹은 멀티 스레드.
  5. 스레드는 프로그램의 흐름(코드의 흐름).
  6. 프로세스들은 독립적으로 존재해서 하나의 프로세스가 문제가 생겨도 다른 프로그램에 영향을 주지 않지만 스레드는 하나의 스레드에 문제가 생기면 스레드를 생성한 프로그램 자체가 종료된다.
  1. 자바에서는 메인 메소드를 진행하는 스레드를 메인 스레드라고 부르며, 메인 스레드가 진행되면서 다른 스레드를 생성하면 스레드가 2개이상 되기 때문에 멀티스레드 프로그래밍라고 부르게 된다.
  1. 스레드의 동작 방식
  • 스레드의 작업 크기(어떤 스레드가 먼저실행되고 먼저 끝나는지)는 스레드 스케쥴러에 의해서만 결정된다. 개발자가 직접 지정할 수 없다. 개발자는 스레드가 대기큐에 등록되는 순서만 지정할 수 있다. 하지만 join()이나 일시정지 시키는 기능을 통해서 실행되는 순서는 지정할 수 있다.

2. 스레드의 정의

  1. Thread 클래스로 직접 생성
    Thread thread = new Thread(Runnable task);
    
    class Task implements Runnable {
    @Override
        public void run() {
            //스레드가 처리할 소스코드
        }
    }

    Runnable task = new Task();
    Thread thread = new Thread(task);
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {

        }
    });
  1. Thread 클래스를 상속받은 자식클래스 생성
    class ChildThread extends Thread {
        @Override
        public void run() {
            //스레드가 실행할 코드
        }
    }
    Thread thread = new ChildTread();
    thread.start();
  1. Thread의 익명클래스 사용
    Thread t = new Thread() {
        @Override
        public void run() {
            //스레드가 실행할 코드
        }
    }

3. 스레드의 사용

  1. start() : 메소드를 사용하여 해당 스레드를 실행 대기 큐에 넣는다. 먼저 실행되고 있는 스레드가 존재하면 실행중인 스레드가 종료돼고 실행됨.
  2. 큐(Queue) : FIFO(First In First Out)으로 동작하는 자료구조. 먼저 큐에 저장된 내용이 먼저실행.
  3. 스택(Stack) : FILO(First In Last Out)으로 동작하는 자료구조. thread.start();

4. 스레드의 이름

  1. main 메소드에서 동작하는 스레드는 main이라는 이름을 가지고 있지만 서브 스레드에는 특정 이름이 생성되지 않고 Thread-1, 2, .... n 이름이 생성된다.
  2. setName(스레드 이름) : 스레드의 이름을 설정
  3. 디버깅 시 현재 어떤 스레드가 실행되고 있는 지 확인할 때 스레드 이름을 사용한다.
  4. Thread.currentThread(); : 현재 동작중인 스레드에 대한 객체를 얻어온다.

5. 스레드 용어 정리

  1. 스레드 : 프로그램 내의 코드의 흐름(작업의 단위). for(10회 반복) 같은 코드를 스레드로 생성할 수 있다.
  2. 멀티 스레드 : 스레드를 여러개 생성해서 하나 씩 스레드를 실행, 정지시키면서 병렬처리. 하이퍼 스레딩이 지원되지 않는 CPU에서 주로 사용.
  3. 논리 코어 : 물리적인 코어 1개를 가상의 여러개의 코어를 만들어서 처리하는 기술. 대부분 2의 거듭제곱으로 이뤄진다.(듀얼 코어 : 2 논리코어, 쿼드 코어 : 4 논리코어, 옥타코어 : 8 논리코어) 하지만 최근에 나오는 CPU들은 아키텍쳐가 변경돼서 효율코어(8) + 성능코어(8) => 16개의 코어를 갖는 CPU가 발매되고 있다. 성능코어와 효율코어가 논리코어에 포함되는 개념은 아니고 아예 다른 개념의 코어다. 위 세 개의 코어는 설계 목적에 따라 분류한다. 논리코어는 하이퍼스레딩을 위해 설계된 코어. 성능코어는 CPU의 최대성능을 끌어내기 위해 설계된 코어. 효율코어는 전력소비를 줄이기 위해서 설계된 코어.
  4. 논리 프로세서(스레드) : 스레드의 작업을 처리할 수 있는 가상의 프로세서. 대부분 논리코어당 2개의 논리 프로세서를 가질 수 있어서 CPU 전체에서 논리코어 * 2개수만큼 존재한다.
  5. 하이퍼 스레딩 : 실제로 스레드를 동시에 실행시키는 기능. 하지만 물리코어가 여러개인 CPU에서만 가능. 물리코어가 하나인 PC에서는 멀티 스레드와 비슷한 방식으로 진행됨. 여러 개의 코어를 이용해서 코어당 2개의 논리 프로세서를 생성하고 2개의 논리 프로세서가 스레드를 하나 씩 점유하여 처리하는 방식.(물리코어가 여러 개인 CPU에서는 동시 처리, 물리코어가 하나인 CPU에서는 멀티 스레드 방식과 비슷한 방식으로 처리)
  6. 스레드 작업 순서는 CPU가 지정하기 때문에 개발자는 확인할 수 없다. 다만 sleep(), wait(), notify()등을 이용해서 원하는 순서대로 작업 순서를 정할 수 있다.

6. 스레드의 상태

  1. start() 메소드를 호출하면 실행 대기 상태(RUNNABLE)로 변경됨.
  2. sleep() 메소드를 호출하면 일시정지 상태(PAUSE)
  3. 스레드 상태의 흐름
    • 스레드 객체를 만들고 스레드가 작업할 내용을 오버라이드된 run() 메소드 안에 구현 -> start() 메소드 호출 -> 실행 대기 상태로 변경 -> 본인 작업 순서가 되면 run() 메소드 구현되어 있는 내용을 한 번 실행 -> run() 메소드 호출이 종료되면 실행 대기 상태로 변경 -> 다른 스레드가 작업 한 번 실행 -> 다른 스레드가 실행 대기 상태가 되면 생성한 스레드의 작업이 다시 한 번 진행 -> 실행 대기 상태 -> ..... -> run() 메소드에 구현되어 있는 작업이 더이상 존재하지 않을 때는 종료 상태(TERMINATED)로 변경
    • 위의 상태 흐름에는 등장하지 않지만 일시 정지 상태도 존재한다. 일시 정지 상태는 해당 스레드를 동작시킬 수 없는 상태. 일시 정지 상태인 스레드는 다시 실행 대기 상태로 옮겨줘야한다. sleep() 메소드 같은 경우는 시간이 지나면 자동으로 실행 대기 상태가 되는 반면 다른 메소드를 사용해서 일시 정지 상태로 변경하면 다른 방식으로 실행 대기 상태로 변경해줘야 됨.
  4. 일시 정지 상태로 만드는 메소드
    • sleep(long milliseconds) : static으로 선언되어 있어서 객체생성 없이 바로 호출 가능. try~catch로 항상 감싸줘야 한다. 매개변수로 받아온 시간만큼 시간이 흐르면 자동으로 실행 대기 상태로 변경된다.
    • join() : 이 메소드를 호출한 스레드를 일시 정지 상태로 만듦. 실행 대기 상태로 변경하려면 join()메소드를 소유한 스레드가 종료되어야 한다.
        Thread t1 = new Thread() {
            @Override
            public void run() {
    
            }
        }; 
    
        Thread t2 = new Thread() {
            @Override
            public void run() {
                
            }
        }; 
    
        t1.start();
        t2.start();
    
        t2.join(); //t1이 진행되는 상태에서 호출되기 때문에
                   //t1이 일시정지 상태가 됨.
                   //t2의 작업이 모두 종료되면 t1이 실행대기 
                   //상태가 된다.
        어떤 스레드가 CPU를 점유했을 때 호출했는지와 어떤 스레드가 끝나야 다시 실행대기 상태로 가는지 주의깊게 살펴야한다.
    
    • wait() : 동기화 블록에서 스레드를 일시 정지 상태로 만듦.
  5. 일시 정지 상태인 스레드를 실행 대기 상태로 변경하는 메소드
    • interrupt() : 일시 정지 상태일 때 InterruptExeception을 발생시켜 해당 스레드를 실행 대기 상태나 종료 상태로 변경함.
    • notify(), notifyAll() : wait() 메소드를 통해 일시 정지 상태로 변경된 스레드를 실행 대기 상태로 만듦.
  6. 실행 상태에서 실행 대기 상태로 변경하는 메소드
    • yield() : 다른 스레드한테 실행 순서를 양보하고 본인은 실행 대기 상태로 변경됨.

7. 스레드의 동기화

  1. 각각 다른 스레드에서 하나의 객체를 공유해서 사용할 때 동기화가 필요하다.
  2. 스레드1에서 객체의 변수를 사용하고 일시정지 상태로 변경 -> 객체의 멤버변수가 스레드1이 점유한 상태(num = 10; add() {num+num;}) -> 스레드1을 sleep(), join()을 사용해서 일시 정지 상태로 변경 -> 스레드2에서 객체의 변수의 값을 변경(num = 20; mul() {num*num;}) -> 스레드2가 400으로 값을 받는 작업을 끝내고 -> 스레드1이 실행되는데 원하는 결과인 20이 나오지 않고 40이 나오게 됨.
  3. 위의 경우에서 사용중인 객체를 다른 스레드가 변경하지 못하도록 잠금처리를 할 수 있다. 자바에서는 동기화 메소드와 동기화 블록을 제공.
  4. 첫 스레드에서 동기화 블록이나 동기화 메소드를 호출하면 해당 블록이나 해당 메소드를 첫 스레드가 점유하기 때문에 다른 스레드에서는 동기화 블록이나 동기화 메소드는 사용하지 못하고 일반 메소드만 호출 가능.

8. 동기화 블록/메소드 선언

  1. 동기화 블록 선언
    메소드() {
        synchronized(공유객체의 변수명) {
            //단 하나의 스레드에서만 실행되는 코드
        }
    }
  1. 동기화 메소드 선언
    public synchronized 리턴타입 메소드명(매개변수) {
        //단 하나의 스레드에서만 실행되는 코드
    }

9. 두 개 이상의 스레드를 교대로 실행하고 싶을 때

  1. wait()와 notify()을 사용해서 교대로 스레드의 작업을 진행할 수 있다.
  2. 본인의 작업이 한 번 실행되면 wait()를 호출해서 일시 정지 상태로 만들고 notify()를 호출해서 다른 스레드를 실행 대기 상태로 변경
  3. 위 방식의 핵심은 공유객체를 이용하는 것이다. 공유객체 안에 두 개의 스레드가 작업할 내용을 미리 정의해놓고 작업이 끝나는 부분에 wait()와 notify()를 호출하여 스레드의 상태를 계속 변경해주는 방식으로 구현.
  4. wait(); notify();
  5. wait(), notify()는 동기화 메소드나 동기화 블록에서만 사용가능하다.

10. 스레드의 안전 종료

  1. run() 메소드의 코드가 모두 실행되면 스레드는 자동 종료된다.
  2. 종종 run() 메소드의 코드가 모두 실행되지 않은 상태의 스레드를 종료시켜야 하는 경우가 발생한다.
  3. 스레드를 강제 종료하는 방식
    • 조건문을 사용하는 방식
      • boolean형의 변수를 하나 선언, run() 메소드 안에 while(변수가 false) {}, 특정조건이 됐을 때 변수를 true로 변경해서 while문을 종료하는 방식
          class ThreadA extends Thread {
              private boolean stop = false;
      
              getter/setter
      
              @Override
              public void run() {
                  while(!stop) {
      
                  }
                  //리소스정리 코드
              }
          }
      
    • intterupt() 메소드 사용하는 방식
      • 스레드.start();를 호출하여 실행 대기 상태로 만든 후에 스레드.interrupt(); 메소드를 호출하면 해당 스레드가 일시정지 상태가 될 때 InterruptedExeception이 발생된다. 예외가 발생하면서 스레드가 종료되고 catch구문안에서 리소스를 정리할 수 있게 된다.
  4. stop() 메소드가 사용중지가 된 이유 : stop() 메소드는 스레드에서 어떤 리소스를 점유하고 있든지 상관없이 무조건 스레드를 강제종료 한다. 그래서 점유 리소스를 해제해주는 작업이 필요할 경우에는 스레드가 종료되기 전에 리소스 정리를 해줘야 하는데 stop() 메소드는 리소스 정리를 할 수 있는 구간이 없었다. 조건문을 이용하거나 intterupt() 메소드를 이용해서 스레드를 종료시킬 때는 리소스를 정리할 수 있는 코드를 추가할 수 있기 때문에 2가지 방식으로 스레드를 종료하도록 하고 있다.

11. 데몬 스레드

  1. 데몬 스레드는 주 스레드의 작업을 도와주는 보조 스레드. 주 스레드가 종료되면 데몬 스레드도 자동으로 종료된다. 편집기(word, 한글) 프로그램 자체를 주 스레드라고 생각하면 자동 저장기능이 프로그램 수행을 돕는 데몬 스레드(보조 스레드)의 역할을 한다.

12. 스레드 풀

  1. 풀 : 수영장
  2. 스레드 풀 : 스레드가 많이 있는 수영장. 스레드를 수영장에서 하나 씩 꺼내서 쓰는 방식. 개발자가 지정한 개수만큼의 스레드를 풀로 만들어서 스레드가 필요할 때 대여해줬다가 다시 돌려받는 형태의 스레드 처리방식.
  3. 스레드 풀의 장점
    • 동시에 많은 사용자가 접속해도 사용자만큼 스레드를 만들지 않고 지정한 개수만큼만 스레드를 운용할 수 있어서 메모리 부족현상이 발생할 확률이 줄어듬
  4. 스레드 풀 생성
    • 자바에서는 java.util.concurrent 패키지에 있는 ExecutorService 인터페이스와 Executors 클래스를 제공.
    • Executors의 static 메소드 두 개를 이용하면 간단하게 스레드 풀인 ExecutorService 구현 객체를 만들 수 있다.
    • newCachedThreadPool() : 초기 수 0, 코어 수 0, 최대 수 Integer.MAX_VALUE으로 설정되어 있는 스레드풀을 생성
    • newFixedThreadPool(int nThreads) : 초기 수 0, 코어 수 만들어진 스레드 개수, 최대 수 nThreads으로 설정된 스레드 풀을 생성.
    • 위 두 메소드 사용하지 않고 스레드 풀을 만드는 방식
        ExecutorService threadPool = new ThreadPoolExecutor(
            코어 스레드 개수,
            최대 스레드 개수,
            스레드가 생성돼서 작업이 없어도 되는 시간,
            위에서 지정한 시간의 단위(TimeUnit.Second),
            작업 큐(new SynchronizedQueue<Runnable>())
        );
    
  5. 스레드 풀 종료
    • 스레드 풀은 메인 스레드의 데몬 스레드가 아니기 때문에 메인 스레드가 종료되도 작업이 계속 진행된다.
    • 메인 스레드가 끝나면서 작업이 계속 진행되지 않도록 하려면 스레드 풀의 모든 스레드를 종료해야한다.
        ExecutorService의 두 개의 메소드를 사용해서 종료한다.
        void shutdown() : 현재 처리중인 작업뿐만 아니라 큐에 대기하고 있는 작업들을 모두 처리한 후에 스레드 풀 종료
        List<Runnable> shutdownNow() : 현재 처리중인 스레드를 interrupt해서 작업을 중단하고 스레드 풀을 종료. 리턴해줄 리스트에 담기는 값은 큐에 남아있던 작업들.
    
    • 남아있는 작업들을 모두 처리하고 스레드 풀을 종료하려면 shutdown() 호출, 지금 바로 스레드 풀을 종료해야 한다면 shutdownNow() 호출.