Java 개념 정리IV

7 분 소요

Java 개념 정리

쓰레드

1. 쓰레드, 프로세스, 멀티 쓰레드

1-1. 프로세스와 쓰레드(process & thread)

  • 프로그램 (실행) -> 프로세스
  • 프로그램: 실행 가능한 파일(HDD, SSD)
  • 프로세스: 실행 중인 프로그램(메모리), 자원(resources)과 쓰레드로 구성
  • 쓰레드: 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.
  • 싱글 쓰레드 프로세스 = 자원 + 쓰레드
  • 멀티 쓰레드 프로세스 = 자원 + 쓰레드 + … + 쓰레드

1-2. 멀티프로세스 vs. 멀티쓰레드

  • 멀티 태스킹(멀티 프로세싱): 동시에 여러 프로세스를 실행시키는 것
  • 멀티 쓰레딩: 하나의 프로세스 내에 동시에 여러 쓰레드를 실행시키는 것
  • 프로세스를 생성하는 것보다 쓰레드를 생성하는 비용이 적다.
  • 같은 프로세스 내의 쓰레드들은 서로 자원을 공유한다.

1-3. 멀티쓰레드의 장점

  • 대부분의 프로그램이 멀티쓰레드로 작성되어 있다. 그러나, 단점도 존재
  • 장점:
    • 시스템 자원을 보다 효율적으로 사용할 수 있다.
    • 사용자에 대한 응답성이 향상된다.
    • 작업이 분리되어 코드가 간결해진다.
  • 단점:
    • 동기화(synchronization)에 주의해야 한다.
    • 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.
    • 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.

1-4. 쓰레드의 구현과 실행

  • Tread클래스를 상속
      class MyThread extends Thread{
          public void run(){ //Thread클래스의 run()을 오버라이딩
          }
      }
    
      MyThread t1 = new MyThread(); // 쓰레드의 생성
      t1.start(); // 쓰레드의 실행
    
  • Runnable인터페이스를 구현
      class MyThread2 implements Runnable{
          public void run() { //Runnable인터페이스의 추상메서드 run()을 구현
          }
      }
    
      Runnable r = new MyThread2();
      Thread t2 = new Thread(r); //Thread(Runnable r)
      //Thread t2 = new Thread(new MyThread2());
      t2.start();
    

1-5. start()와 run()

  • class ThreadTest{
        public static void main(String args[]){
            MyThread t1 = new MyThread();
            t1.start();
        } 
    }
    
    class MyThread extends Thread{
        public void run(){
            //...
        }
    }
    

    2. 싱글쓰레드와 멀티쓰레드

2-1. 싱글쓰레드 vs. 멀티스레드

  • 싱글 쓰레드
      class ThreadTest{
          public static void main(String args[]){
              for(int i=0; i<300; i++){
                  System.out.println("-");
              }
              for(int i=0; i<300; i++){
                  System.out.println("|");
              }
          } //main
      }
    
  • 멀티 쓰레드
      class ThreadTest{
          public static void main(String args[]){
              MyThread1 th1 = new MyThread1();
              MyThread2 th2 = new MyThread2();
              th1.start();
              th2.start();
          }
      }
      class MyThread1 extends Thread{
          public void run(){
              for(int i=0;i<300;i++){
                  System.out.println("-");
              }
          }//run
      }
      class MyThread2 extends Thread{
          public void run(){
              for(int i=0;i<300;i++){
                  System.out.println("|");
              }
          } //run
      }
    

2-2. 쓰레드의 우선순위(priority of thread)

  • 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.
      void setProiority(int newProiority) - 쓰레드의 우선순위를 지정한 값으로 변경한다.
      int getPriority() - 쓰레드의 우선순위를 반환한다
        
      public static final int MAX_PRIORITY = 10 //최대우선순위
      public static final int MIN_PRIORTY = 1 //최소우선순위
      public static final int NORM_PRIORITY = 5 //보통우선순위
    

2-3. 쓰레드 그룹(ThreadGroup)

  • 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것 (보안상의 이유)
  • 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 한다.
  • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 ‘main쓰레드 그룹’에 속한다.
  • 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.

2-4. 데몬 쓰레드(daemon thread)

  • 일반 쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행.
  • 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
  • 가비지 컬렉터, 자동저장, 화면자동갱신 등에 사용된다.
  • 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 한다.
      boolean isDaemon() - 쓰레드가 데몬쓰레드인제 확인한다. 데몬쓰레드이면 true를 반환
      void setDaemon(boolean on) - 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경. 매개변수 on을 true로 지정하면 데몬 쓰레드가 된다.
    
  • setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다.

3. 쓰레드의 상태와 실행제어

3-1. 쓰레드의 실행제어

  • 쓰레드의 실행을 제어(스케줄링)할 수 있는 메서드가 제공된다. 이 들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.
      static void sleep(long millis) - 지정된 시간동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다.
    
      void join() - 지정된 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
    
      void interrupt() - sleep()이나 join에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 interruptedException이 발생함으로써 일시정지상태를 벗어나게 된다.
    
      void stop() - 쓰레드를 즉시 종료시킨다.
    
      void suspend() -쓰레드를 일시 정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
    
      void resume() - suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
    
      static void yield() - 실행중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.
    
  • resume(), stop(), suspend()는 쓰레드를 교착상태로 만들기 쉽기 때문에 deprecated되었다.

3-2. 쓰레드의 상태(state of thread)

  • NEW : 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능한 상태
  • BLOCKED : 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때 까지 기다리는 상태)
  • WAITING, TIMED_WATING : 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다.
  • TERMINATED : 쓰레드의 작업이 종료된 상태 쓰레드상태

3-3. 쓰레드의 실행제어 메서드

sleep()

  • 현재 쓰레드를 지정된 시간동안 멈추게 한다.
      static void sleep(long millis) //천분의 일초 단위
      static void sleep(long millis, int nanos) //천분의 일초 + 나노초
    
  • 예외처리를 해야 한다. (InterruptedException이 발생하면 깨어남)
      try{
          Thread.sleep(1, 500000); //쓰레드를 0.0015초 동안 멈추게 한다.
      } catch(InterruptedException e){}
    
  • 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.
      try{
          //th1.sleelp(2000);  불가
          Thread.sleep(2000);
      }catch(InterruptedException e){}
    

    interrupt()

  • 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
  • void interrupt() - 쓰레드의 interrupted상태를 false에서 true로 변경
  • boolean isInterrupted() - 쓰레드의 interrupted 상태를 반환
  • static boolean interrupted() - 현재 쓰레드의 interrupted 상태를 알려주고, false로 초기화
      class Thred{
          boolean interrupted = false;
          boolean isInterrupted(){
              return interrupted;
          }
          boolean interrupt(){
              interrupted = true;
          }
      }
    

suspend(), resume(), stop()

  • 쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다. 교착상태에 빠지기 쉽다.
  • void suspend() - 쓰레드를 일시정지 시킨다.
  • void resume() - suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.
  • void stop() - 쓰레드를 즉시 종료시킨다.
  • suspend(), resume(), stop()은 deprecated되었으므로, 직접 구현해야 한다.
      class Thread implements Runnable{
          boolean suspended = false;
          boolean stopped = false;
          public void run(){
              while(!stopped){
                  if(!suspended){
                      /* 쓰레드가 수행할 코드를 작성 */
                  }
              }
          }
          public void suspended{ suspended = true; }
          public void reusme{ suspended = false; }
          public void stop(){ stopped = true; }
      }
    

    yield()

  • 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기한다.
  • yield()와 interrupt()를 적절히 사용하면, 응답성과 효율성을 높일 수 있다.
      class MyThread implements Runnable{
          boolean suspended = false;
          boolean stopped = false;
    
          Thread th;
    
          MyThread(String name){
              th = new Thread(this, name);
          }
          public void run(){
              while(!stopped){
                  if(!suspended){
                      /* 작업수행 */
                      try{
                          Thread.sleep(1000);
                      }catch(InterruptedException e){}
                  }else{
                      Thread.yield();
                  }
              }
          }
      }
    

    join()

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
  • void join() - 작업이 모두 끝날 때까지
  • void join(long millis) - 천분의 일초 동안
  • void join(long millis, int nanos) - 천분의 일초 + 나노초 동안
  • 예외처리를 해야 한다. (InterruptedException이 발생하면 작업 재개)
      public static void mina(String args[]){
          Thread1 th1 = new Thread1();
          Thread2 th2 = new Thread2();
          th1.start();
          th2.start();
          startTime = System.currentTimeMillis();
    
          try{
              th1.join(); //main쓰레드가 th1의 작업이 끝날 때까지 기다린다.
              th2.join(); //main쓰레드가  th2의 작업이 끝날 때까지 기다린다.
          }catch(InterruptedException e){}
      }
    

    4. 쓰레드의 동기화

4-1 쓰레드의 동기화 - synchronized

  • 한 번에 하나의 쓰레드만 객체에 접근할 수 있도록 객체에 락(lock)을 걸어서 데이터의 일관성을 유지하는 것
      //특정한 객체에 lock을 걸고자 할 때
      synchronized(객체의 참조변수){
    
      }
      public void withdraw(int money){
          synchronized(this){
              if(balance >= money){
                  try{
                      Thread.sleep(1000);
                  }catch(Exception e){}
                  balance -= money;
              }
          }
      }    
        
      //메서드에 lock을 걸고자 할 때
      public synchronized void calcSum(){
    
      }
      public synchronized void withdraw(int money){
          if(balance >= money){
              try{
                  Thread.sleep(1000);
              }catch(Exception e){}
              balance -= money;
          }
      }    
    

4-2 wait(), notify(), notifyAll()를 이용한 동기화

  • 동기화의 효율을 높이기 위해 wait(), notify()를 사용
  • Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
  • wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
  • notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
  • notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨운다.
      class Account{
          int balance = 1000;
    
          public synchronized void withdraw(int money){
              while(balance < money){
                  try{
                      wait(); // 대기 - 락을 풀고 기다린다. 통지를 받으면 락을 재획득
                  }catch(InterruptedException e){}
              }
              balance -= money;
          }
          public synchronized void deposit(int money){
              balance += money;
              notify(); // 통지 - 대기중인 쓰레드 중 하나에게 알림
          }
      }
    

    5. Lock & Condition

5-1. Lock과 Condition을 이용한 동기화

  • java.util.concurrent.locks패키지를 이용한 동기화 (JDK 1.5)
  • ReentrantLock - 재진입이 가능한 lock. 가장 일반적인 배타 lock
  • ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 배타적인 lock
  • StampedLock - ReentrantReadWriteLock에 낙관적인 lock의 가능을 추가
  • 낙관적인 잠금(Optimistic Lock): 일단 저지르고 나중에 확인
      int getBalance(){
          long stamp = lock.tryOptimisticRead(); //낙관적 읽기 lock을 건다.
          int curBalance = this.balance; //공유 데이터인 balance를 읽어온다.
          if(lock.validate(stamp)){ //쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
              stamp = lock.readLock(); //lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.
    
              try{
                  curBalance = this.balance; //공유 데이터를 다시 읽어온다.
              }finally{
                  lock.unlockRead(stamp); //읽기 lock을 푼다.
              }
          }
          return curBalance; //낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
      }
    
  • ReentrantLock을 이용한 동기화
    • ReentrantLock()
    • ReentrantLock(boolean fair)
  • synchronized 대신 lock()과 unlock()을 사용
    • void lock() - lock을 잠근다.
    • void unlock() - lock을 해지한다.
    • boolean isLocked() - lock이 잠겼는지 확인한다.

5-2. volatile - cache와 메모리간의 불일치 해소

  • 성능 향상을 위해 변수의 값을 core의 cache에 저장해 놓고 작업
  • 여러 스레드가 공유하는 변수에는 volatile을 붙여야 항상 메모리에서 읽어옴
      volatile boolean suspended = false;
      volatile boolean stopped = false;
    
      public synchronized void stop(){
          stopped = true;
      }
    

    6. join & fork 프레임웍

6-1. fork & join 프레임웍

  • 작업을 여러 쓰레드가 나눠서 처리하는 것을 쉽게 해준다(JDK 1.7)
  • RecursiveAction 또는 RecursiveTask를 상속받아서 구현
  • RecursiveAction - 반환값이 없는 작업을 구현할 때 사용
  • RecursiveTask - 반환값이 작업을 구현할 때 사용

6-2. compute()의 구현

  • 수행할 작업과 작업을 어떻게 나눌 것인지를 정해줘야 한다.
  • fork()로 나눈 작업을 큐에 넣고, compute()를 재귀호출 한다.
      public Long compute(){
          long size = to - from + 1;
          if(size<=5){
              return sum();
          }
          long half = (form+to)/2;
    
          SumTask leftSum = new SumTask(from, half);
          SumTask rightSum = new SumTask(half+1, to);
    
          leftSum.fork(); ///작업을 작업 큐에 넣는다.
    
          return rightSum.compute() + leftSum.join();
      }
    

    6-3. 작업 훔치기(work stealing)

  • 작업을 나눠서 다른 쓰레드의 작업 큐에 넣는 것 쓰레드

6-4. fork()와 join()

  • compute()는 작업을 나누고, fork()는 작업을 큐에 넣는다. (반복)
  • join()으로 작업의 결과를 합친다. (반복)
  • fork() - 해당 작업을 쓰레드 풀의 작업 큐에 넣는다. 비동기 메서드
  • join() - 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 결과를 반환한다. 동기메서드

태그:

카테고리:

업데이트: