10주차 : 멀티쓰레드 프로그래밍

Joo·2023년 4월 19일

백기선 자바 스터디

목록 보기
10/13

1. Thread

프로세스 내의 실행 흐름(작업) 단위
프로세스 & 쓰레드

  • 자바 프로그램 시작 → 프로세스 시작 → 여러 개의 쓰레드가 실행됨
    • main 메소드가 시작하면서 하나의 쓰레드가 시작됨
    • 만약 많은 쓰레드가 필요하다면?
      • main 메소드에서 쓰레드를 생성해서 사용함
      • 톰캣 같은 WAS도 main 메소드에서 실행한 여러 쓰레드를 수행하는 것
  • 사용 목적
    • 하나의 작업동시에 수행하기 위해서
    • 동시 작업을 위해 여러 프로세스를 실행하는 것은 비용이 매우 많이 들어감
    • 쓰레드는 프로세스에 비해 훨씬 적은 비용이 들어감
      • 쓰레드 = 경량 프로세스

2. Runnable 인터페이스 & Thread 클래스

쓰레드를 생성 및 실행하기 위해 필요한 클래스와 인터페이스

  • 둘 다 java.lang에 있음
  • 쓰레드의 동작은 Runnable 인터페이스의 run()
    • 오버라이딩하여 쓰레드가 동작할 행동을 정의해야 함
  • 쓰레드의 시작은 Thread 클래스의 start()

2.1 Runnable 인터페이스

  • run() 메소드 하나만 존재하는 인터페이스 → Functional Interface
    @FunctionalInterface
    public interface Runnable {
        
        public abstract void run();
    }
  • Runnable 인터페이스를 구현한 클래스로 바로 쓰레드를 시작할 순 없음
    1. Thread 객체를 만들고 생성자 매개변수로 Runnable 구현체를 넣어줌

    2. start() 메소드 실행

      public class RunnableImpl implements Runnable {
      
          @Override
          public void run() {
              System.out.println("hi Runnable");
          }
      }
      public class Basic {
      
          public static void main(String[] args) {
              RunnableImpl runnable = new RunnableImpl();
      
              new Thread(runnable).start();
          }
      }
      

2.2 Thread 클래스

  • Runnable 인터페이스를 구현한 클래스
  • run() 메소드를 오버라이딩하고 start() 메소드를 호출하여 쓰레드 시작
    public class ThreadEx extends Thread{
    
        @Override
        public void run() {
            System.out.println("hi Thread");
        }
    }
    package week10_thread.basic;
    
    public class Basic {
    
        public static void main(String[] args) {
            ThreadEx thread = new ThreadEx();
    
            thread.start();
        }
    }

start() & run()

  • start()
    • 새로운 stack frame을 생성
    • 여기서 run() 호출
  • run()
    • 쓰레드가 실행할 로직이 구현된 메소드

출처 : https://wisdom-and-record.tistory.com/48

2.3 왜 2가지 방법으로 쓰레드를 구현할까?

  • 쓰레드로 사용하려는 클래스가 다른 클래스의 상속을 받아야 할 경우
    • 다중 상속을 받을 수 없으므로 Thread 클래스를 상속받을 수 없음 → Runnable 인터페이스를 구현해서 사용
    • 그렇지 않은 경우에는 Thread 클래스를 바로 상속받아서 더 편하게 쓰레드를 사용할 수 있음

2.4 쓰레드 실행 결과

package week10_thread.basic;

public class Basic {

    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        ThreadEx thread = new ThreadEx();

        // 세 줄의 실행 결과가 순서가 보장되지 않음
        new Thread(runnable).start();   // hi Runnable
        thread.start();                 // hi Thread
        System.out.println("hi main");
    }
}
  • 프로그램 실행 결과 위의 출력하는 코드 3줄의 실행 순서가 보장되지 않음
    • 쓰레드는 다른 쓰레드의 종료 시점까지 기다리지 않고 자기 차례가 되면 바로 시작함
  • thread.start()
    • JVM에 쓰레드를 추가하여 실행한다는 의미
    • 각 쓰레드의 run() 메소드 시작 순서는 동일
      • but, 종료 순서는 다름! → 실행 결과의 순서가 보장되지 않는 이유

2.5 Thread 생성자

  • 매개변수
    • ThreadGroup group
      • group에 속하는 쓰레드를 생성
        • 쓰레드 생성 시 지정한 그룹으로 묶는 것
      • ThreadGroup으로 묶어놓으면 여러 메소드를 통해 다양한 정보를 얻을 수 있음
    • Runnable target
      • target 객체가 구현한 run() 메소드를 실행하는 쓰레드를 생성
    • String name
      • 쓰레드의 이름을 지정
      • 모든 쓰레드에는 이름이 있음
        • 미지정 시 ‘Thread-n’
        • n은 생성된 순서에 따라 증가함
        • 쓰레드의 이름이 겹쳐도 에러나 예외가 발생하지 않음
    • long stackSize
      • 해당 쓰레드의 최대 스택 크기를 지정

2.6 쓰레드에서 매개변수를 받고 싶은 경우

  • 생성자매개변수를 받아서 처리
    public class ThreadEx extends Thread{
    
        int number;
    
        public ThreadEx(int param) {
            number = param;
        }
       
        @Override
        public void run() {
            System.out.println("hi Thread");
            System.out.println("파라미터로 넘겨받은 " + number + "을 run에서 사용");
        }
    }
    public class Basic {
    
        public static void main(String[] args) {
    
            ThreadEx paramThread = new ThreadEx(10);
            paramThread.start();
    
            System.out.println("hi main");
        }
    }

3. Thread 상태

출처 : https://sujl95.tistory.com/63

3.1 NEW

쓰레드 객체가 생성되고 아직 시작되지 않은 상태

3.2 RUNNABLE

쓰레드 스케줄러에 의해 쓰레드가 실행되도록 지정된 상태입니다.

  • 쓰레드가 JVM에서 실행 중인 상태
    • 프로세서와 같은 운영 체제의 다른 리소스를 기다리고 있을 수 있음

3.3 BLOCKED

쓰레드 실행 중지 상태

  • monitor lock을 기다리는 상태
    • synchronized 블록 또는 메소드에 들어가길(or reenter) 기다리는 상태
      = 사용하려고 하는 리소스의 락이 해제되길 기다리는 상태

3.4 WAITING

쓰레드가 대기중인 상태

  • wait(), join() 메서드 등을 통해 대기하고 있는 상태
    • 다른 쓰레드의 notify() 또는 notifyAll() 호출을 기다리고 있음

3.5 TIMED_WAITING

쓰레드가 특정 시간만큼 대기중인 상태

  • sleep(), wait(), join() 메소드를 통해 정해진 시간만큼 대기하고 있는 상태
  • 대기 시간이 지나면 다시 Runnable 상태로 돌아감

3.6 TERMINATED

쓰레드가 종료된 상태

  • 실행이 완료되거나, 예외가 발생한 경우에 해당

  • Thread 클래스 내부에 enum으로 선언되어 있음
    public enum State {
            NEW,
    
            RUNNABLE,
    
            BLOCKED,
    
            WAITING,
    
            TIMED_WAITING,
    
            TERMINATED;
        }
  • getState() 메소드를 통해 쓰레드 상태 확인 가능

4. Thread 우선순위

1. MIN_PRIORITY = 1

쓰레드가 가질 수 있는 최소 우선순위

2. NORM_PRIORITY = 5

쓰레드가 생성될 때 가지는 기본 우선순위

3. MAX_PRIORITY = 10

쓰레드가 가질 수 있는 최대 우선순위

  • Thread 클래스에 int 상수로 정의되어 있음
  • 쓰레드의 우선순위를 설정하면 높은 우선순위를 갖는 쓰레드가 먼저 실행됨
    • but, 우선순위가 높은 쓰레드가 항상 먼저 실행된다고 보장할 수 없음!
  • 그래도 사용하는 이유는 중요한 작업을 하는 쓰레드에 더 많은 CPU 시간을 제공해 속도와 효율성을 높이기 위함임

5. Main Thread

자바 프로세스를 시작하는 main() 메소드를 실행하는 쓰레드

  • 싱글 쓰레드 애플리케이션
    • 메인 쓰레드만 사용하는 애플리케이션
  • 멀티 쓰레드 애플리케이션
    • 메인 쓰레드에서 여러 쓰레드를 생성해 실행하는 애플리케이션

6. Demon Thread

백그라운드에서 메인 쓰레드를 보조하는 쓰레드

  • 메인 쓰레드가 종료되면 데몬 쓰레드도 종료됨
    • 데몬 쓰레드는 자신의 작업이 끝나지 않아도 다른 실행중인 쓰레드가 없으면 멈춤
  • 사용하는 경우
    • 다른 쓰레드에 서비스를 제공할 때
    • 사용자의 입력이 필요하지 않은 작업을 수행할 때
  • 사용법
    • thread.setDaemon(true)
  • 사용 목적
    • 메인 쓰레드 종료 전까지 실행하다가 메인 쓰레드 종료 시 프로세스가 종료되게 하고 싶을 때 사용
      ex) 모니터링 쓰레드
      • 모니터링을 위해 계속 실행중인 상태
      • 프로세스를 종료하기 위해 메인 쓰레드를 종료하면 같이 종료되어야 함
        • 이 때 모니터링 쓰레드를 데몬 쓰레드로 설정하지 않으면 프로세스가 종료되지 않음

7. 동기화 (Synchronization)

7.1 Race Condition

여러 프로세스나 쓰레드가 공유 자원에 동시에 접근하려고 할 때,
접근 순서나 타이밍에 따라 실행 결과가 달라질 수 있는 상황

  • 이로 인해 의도하지 않은 결과가 발생할 수 있음
  • 임계 영역을 지정해 상호 배제를 보장함으로써 race condition을 예방할 수 있음

7.2 Critical Section (임계 영역)

공유 자원의 일관성을 보장하기 위해 하나의 프로세스나 스레드만 진입해서 실행 가능한 코드 영역

  • 임계 영역 조건
    1. 상호 배제 (Mutual Exclusion)
      • 한번에 한 개의 쓰레드만 접근 가능함
      • 여러 쓰레드가 임계 영역에 접근할 경우 실행되는 쓰레드를 제외한 나머지는 대기해야 함
    2. 진행 (Progress)
      • 임계 영역이 비어있고 임계 영역에 들어가길 원하는 프로세스나 스레드들이 있다면, 그 중 하나는 임계 영역에서 실행될 수 있도록 해야 함
    3. 한정된 대기 (Bounded Waiting)
      • 어떤 프로세스나 스레드가 무한정 임계 영역에 들어가지 못하고 기다리고 있으면 안 됨
  • 임계 영역을 구현하기 위해 동기화 기법을 사용해야 함
    • 자바의 경우 monitor를 통해 동기화

7.3 Synchronization

여러 프로세스나 쓰레드가 공유 자원에 동시에 접근해도 공유 자원의 일관성을 유지하는 것

  • 동기화를 통해 공유 자원의 일관성을 유지하려면 임계 영역을 제대로 구현해야 함

  • 다양한 동기화 기법이 존재 (쓰레드 → 프로세스 & 쓰레드를 의미함)

    1. (Lock)

      • 공유 자원에 접근하기 전에 잠금을 획득하고, 접근 후에 잠금을 해제하는 방식으로 상호 배제를 보장하는 동기화 기법
      • 락은 임계 영역이 비어있을 때만 획득할 수 있음
        • 임계 영역이 잠겨있는 동안 다른 쓰레드는 Busy Waiting 상태
          • Busy Waiting
            • CPU를 점유하면서 계속해서 임계 영역의 상태를 확인하는 것
              → CPU 자원을 낭비하게 됨
    2. 뮤텍스 (Mutex)

      • 락과 비슷하지만, 잠금을 획득한 프로세스나 스레드만잠금을 해제할 수 있다는 점이 다른 동기화 기법
      • 뮤텍스 == 이진 세마포어 (Binary Semaphore)
      • 락과 마찬가지로 Busy Waiting 문제가 있음
    3. 세마포어(Semaphore)

      • 공유 자원이 여러 개 존재하는 상황에서도 적용이 가능한 동기화 기법

      • 임계 영역에 진입할 수 있는 쓰레드의 수를 제한하는 카운터를 사용
        상호 배제 X

        💡세마포어의 크기가 1보다 큰 경우, 상호 배제를 보장하지는 않지만 공유 자원의 일관성은 유지할 수 있으므로 동기화 기법이라고 할 수 있음!

        • 엄밀히 말하면 임계 영역을 구현하는 것이 아님 → 여러 쓰레드가 접근할 수 있으므로
      • 대기 큐를 사용하여 Busy Waiting 문제를 해결함 (이진 세마포어 제외)

        → CPU 자원을 절약

    4. 모니터(Monitor)

      • 공유 자원과 쓰레드 사이에 인터페이스를 두고, 인터페이스에 접근하기 위한 큐를 사용하는 동기화 기법
      • 모니터는 임계 영역을 추상화한 객체로, 임계 영역 내부에서만 실행될 수 있는 특별한 함수들을 제공
      • 조건 변수(Condition Variable)를 사용하여 프로세스나 스레드의 실행 순서를 제어할 수 있음
💡 뮤텍스, 세마포어, 모니터 모두 Lock을 사용하는 동기화 기법!

cf) 뮤텍스 vs 모니터

7.4 synchronized

자바에서 메소드나 블록임계 영역으로 지정하는 키워드

  • 쓰레드가 synchronized 메소드나 블록에 진입해 코드를 실행하려면 monitor라는 lock을 획득해야 함
    • 자바의 모든 객체는 1개의 monitor를 가지고 있음
    • monitor
      • mutex (lock)
        • 상호 배제를 보장하기 위해 필요
        • 하나의 쓰레드가 lock를 가지고 있으면 다른 쓰레드는 lock를 획득하지 못함
          • 쓰레드가 entry queue에 들어가고 lock이 회수되면 빠져나옴
      • condition variable
        • 쓰레드의 실행 순서를 제어하기 위해 필요
          • 쓰레드를 특정 조건이 만족할 때까지 대기시키거나
            • wait() → waiting queue에 들어감
          • 대기중인 다른 쓰레드를 깨우거나
            • notify(), notifyAll()
        • 하나의 condition variable 당 하나의 waiting queue가 존재
        • 자바의 condition variable은 1개임
          • 2개 이상 사용하고 싶은 경우 따로 구현해야 함
  • 하나의 쓰레드가 monitor를 획득한 경우 나머지 쓰레드는 임계 영역에 진입하려면 대기해야 함
    • 하나의 객체2개의 synchronized 메소드가 있는 경우 (a,b 메소드)
      • a 메소드가 하나의 쓰레드(A)에 의해 실행 중

        → 해당 쓰레드가 객체의 lock을 획득

      • b 메소드를 사용하려는 다른 쓰레드(B)는 lock을 획득하지 못하므로 대기해야 함

        package org.example.thread;
        
        public class TestClass {
        
            public synchronized void methodA() throws InterruptedException {
                Thread.sleep(1000);
                System.out.println("hi A");
            }
        
            public synchronized void methodB() throws InterruptedException {
                Thread.sleep(1000);
                System.out.println("hi B");
            }
        
        }
        package org.example.thread;
        
        public class ThreadMain {
        
            public static void main(String[] args) {
                TestClass testClass = new TestClass();
        
                Thread threadA = new Thread(() -> {
                    try {
                        testClass.methodA();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                });
        
                Thread threadB = new Thread(() -> {
                    try {
                        testClass.methodB();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                });
        
                threadA.start();
                threadB.start();
            }
        }
        hi A  // 1초
        hi B  // 2초

7.5 Atomic 클래스

단일 변수에 대해 원자적으로 작업을 수행하는 클래스

  • 원자적 연산으로 cpu 레벨에서 상호 배제를 보장함
    • 자바의 여러 쓰레드가 atomic 변수에 접근할 수 있지만 (Nonblocking)
    • cpu 레벨에서 하나의 쓰레드만 atomic 변수로 연산을 할 수 있음
  • synchronized를 사용하지 않고 동기화 문제를 해결할 수 있음
  • Atomic 클래스는 volatile 변수로 값을 처리함
  • CAS(Compare And Swap) 알고리즘을 사용해 값을 변경함
    • 현재 쓰레드가 가지고 있는 값과 메모리의 값이 같으면 변경하는 방식
  • ex
    package org.example.atomic;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicMain {
    
        public static void main(String[] args) throws InterruptedException {
            AtomicInteger atomicInt = new AtomicInteger(0);
    
    //        int normalInt = 0; 애초에 불가능. 컴파일 에러
            final int[] normalInt = {0};
    
            Thread[] threads = new Thread[100];
    
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(() -> {
                    for (int j = 0; j < 1000; j++) {
                        atomicInt.incrementAndGet(); // 원자적 연산
    
    //                    normalInt++;
                        normalInt[0]++; // 비원자적 연산
    
                    }
                });
    
                threads[i].start();
            }
    
            for (Thread thread : threads) {
                thread.join();
            }
    
            System.out.println("atomicInt: " + atomicInt); // 원하는 값: 100000
            System.out.println("normalInt: " + normalInt[0]); // 원하는 값: 100000
        }
    
    }
    atomicInt: 100000
    normalInt: 95052

8. Object 클래스의 쓰레드 관련 메소드

wait()

  • 다른 쓰레드가 해당 객체에 대해 notify() 또는 notifyAll()을 호출할 때까지 대기하도록 함

notify()

  • 소유한 모니터의 WaitSet에서 대기중인 쓰레드 하나를 깨워서 실행시킴
    • WaitSet → EntrySet → lock 획득

notifyAll()

  • 소유한 모니터의 WaitSet에서 대기중인 모든 쓰레드를 깨워서 실행시킴

9. Thread 클래스 메소드

9.1 join()

  • 한 쓰레드가 다른 쓰레드의 작업이 끝날때까지 기다리게 하는 메소드

9.2 interrupt()

  • 실행중인 쓰레드를 중단시키는 메소드
  • 메소드 호출 시 InterruptedException 예외가 발생함

9.3 상태 확인 메소드

(1) isAlive()

  • 쓰레드가 살아있는지 확인하는 메소드
  • start() 메소드가 끝나지 않았으면 true

(2) isInterrupted()

  • 쓰레드가 인터럽트로 종료되었는지 확인하는 메소드
    • 메소드를 호출하는 쓰레드의 상태를 확인함
      t1.isInterrupted();

9.4 static 메소드

(1) interrupted()

  • 현재 실행중인 쓰레드에 대해 인터럽트로 종료되었는지 확인하는 메소드
    Thread.interrupted();

(1) sleep()

  • static 메소드이지만 메소드를 사용하는 각 쓰레드에 동작하는 메소드
  • 보통 쓰레드의 동작을 정의하는 run() 메소드 내부에서 사용
  • 매개변수
    • long millis
      • 밀리초 단위 숫자 입력 ex) 1000(10^3) 밀리초 → 1초
    • int nanos
      • 나노초 단위 숫자 입력 ex) 1000000000(10^9) 나노초 → 1초
  • InterruptedException (CheckedException)을 발생시킬 수 있기 때문에 예외 처리를 해줘야 함
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("hi Thread");
            System.out.println("파라미터로 넘겨받은 " + number + "을 run에서 사용");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

(2) activeCount()

  • 현재 쓰레드의 쓰레드 그룹에 속한 활성 쓰레드 개수를 반환하는 메소드

(3) currentThread()

  • 현재 실행중인 쓰레드 객체를 반환하는 메소드
    • 쓰레드 생성 시 이름을 지정하지 않을 경우 기본 이름 사용
      • Thread-n

(4) dumpStack()

  • 현재 스레드의 스택 트레이스를 표준 오류 스트림에 출력하는 메소드

10. 교착 상태 (Deadlock)

프로세스가 다른 프로세스의 사용 자원을 무한정 대기하고 있는 상태

10.1 데드락 발생 조건

(1) 상호 배제 (Mutual Exclusion)

  • 하나의 리소스하나의 프로세스만 사용 가능해야 함

(2) 점유 대기 (Hold and Wait)

  • 최소 하나의 리소스를 사용하고 있으면서(Hold)
  • 다른 프로세스가 사용하는 리소스를 대기하고 있는(Wait) 프로세스가 있어야 함

(3) 비선점 (No Preemption)

  • 다른 프로세스가 사용중인 리소스를 강제로 뺏을 수 없어야 함

(4) 순환 대기 (Circular wait)

  • 대기하고 있는 프로세스의 형태가 순환 형태(Circular)를 이루고 있어야 함 ex) A → B → C → A

10.2 데드락 해결 방법

1. 예방

  • 데드락 발생 조건 4가지 중 1개 이상을 막아서 데드락 자체가 발생하지 않도록 하는 방법
  • 교착상태를 완벽히 막을 수 있지만, 자원의 활용도가 낮아지고 비용이 많이 들 수 있음
    • 실효성이 떨어져 잘 사용하지 않는 방법

2. 회피

  • 교착상태가 발생할 가능성이 있는 요청거절하는 방법
  • ex) 은행원 알고리즘
    • 시스템이 안전 상태를 유지할 수 있는 요청만을 수락하고 불안전 상태를 초래할 요청은 나중에 만족될 수 있을 때까지 거절하는 방법

3. 탐지 & 회복

  • 교착상태가 발생하는 것을 허용하고 주기적으로 검사하여 발견된 교착상태를 복구하는 방법
  • 탐지
    • 데드락 발생 여부를 탐지함
  • 회복
    • 데드락이 탐지된 경우 순환 대기를 벗어나 데드락을 해결함
      1. 프로세스 중단
        • 모든 프로세스 중단 → 부분적인 결과가 손실될 위험이 있음
        • 하나씩 중단 → 데드락이 해결될 때까지 하나씩 프로세스를 중단함
      2. 리소스 선점
        • 데드락이 해결될 때까지 다른 프로세스가 점유하고 있는 리소스를 강제로 뺏어옴

Reference

10주차 과제: 멀티쓰레드 프로그래밍

쓰레드 상태

10주차 과제: 멀티쓰레드 프로그래밍

[운영체제] 데드락(Deadlock, 교착 상태)이란?

데드락

모니터란 무엇인가?

java monitor

0개의 댓글