[TIL] #13 Java Thread

phdljr·2023년 10월 20일
0

TIL

목록 보기
13/70
post-custom-banner

프로세스와 스레드

  • 프로세스
    • OS로부터 자원을 할당받는 작업의 단위
    • 실행 중인 프로그램
  • 스레드
    • 프로세스가 할당받은 자원을 이용하는 실행의 단위

Java에서의 스레드

  • Java 프로그램을 실행하면 JVM 프로세스 위에서 실행된다.
  • Java 프로그램 쓰레드는 Java 메인 쓰레드로부터 실행되며 JVM에 의해 실행된다.
    • main 메소드를 실행시키면 메인 쓰레드가 시작된다.
    • 메인스레드가 종료되면 JVM도 종료된다.
      • 메인 스레드는 작업을 다 해도, 다른 스레드가 작업을 마칠 때까지 기다린다.
  • JVM은 사용자 스레드의 작업이 끝나면 데몬 스레드도 자동으로 종료시켜 버린다.

싱글 쓰레드

  • JVM 프로세스 + 메인 스레드

멀티 쓰레드

  • JVM 프로세스 + 메인 스레드 + 여러 개의 다른 스레드
  • 메인 스레드에서 다른 스레드를 생성
  • 응답 스레드와 작업 스레드를 분리하여 빠르게 응답을 줄 수 있다(비동기).
  • 문제점
    • 동기화 문제 발생할 수 있다.
      • 데이터 충돌
    • 교착 상태(데드락)이 발생할 수 있다.
      • 모든 스레드가 서로를 기다리는 상황 ⇒ 프로그램 정지 상
    • 누가 먼저 실행될 지 아무도 모른다.
      • 순서를 정해줄 필요가 있다면, 정해야 한다.

데몬 스레드

  • background에서 실행되는 낮은 우선순위를 가진 스레드
    Thread thread = new Thread();
    tread.setDaemon(true);
    • 다른 스레드에 비해 리소스를 적게 할당받는다.
    • 주로 보조적인 역할을 담당한다.
      • ex) 가비지 컬렉터, 자동 더장, 동영상 및 음악 재생 …
    • 주 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.

사용자 스레드

  • foreground에서 실행되는 높은 우선순위를 가진 스레드
    • ex) 메인 스레드

스레드 우선순위와 스레드 그룹

스레드 우선순위

  • 스레드 작업의 중요도에 따라 스레드의 우선순위를 부여할 수 있다.
    • 우선순위의 숫자가 높을수록 먼저 작업할 가능성이 높다.

      • 무조건 먼저 실행된다는 보장은 없다.
    • Java에서의 스레드 우순순위 범위는 OS가 아니라 JVM이다.

    • 1~10의 값으로 설정해줄 수 있으며, 기본 값은 5(NROM_PRIORITY)이다.

      Thread thread = new Thread();
      tread.setPriotiry(8); // 먼저 실행될 가능성이 높음
      
      Thread thread2 = new Thread();
      tread2.setPriotiry(Thread.MIN_PRIORITY);

스레드 그룹

  • 서로 관련이 있는 스레드들을 그룹을 묶어서 다룰 수 있다.
    • 스레드들은 기본적으로 그룹에 포함되어 있다.

      • JVM이 시작되면 system그룹이 생성되고 스레드들은 기본적으로 system 그룹에 포함된다.
    • 메인 스레드는 system 그룹 하위에 있는 main 그룹에 포함되어 있다.

    • 모든 스레드는 반드시 하나의 그룹에 포함되어 있어야 한다.
      - 그룹을 지정받지 못한 스레드는 자신을 생성한 부모 스레드의 그룹과 우선순위를 상속받게 된다.

      ThreadGroup group1 = new ThreadGroup("Group1");
      
      Thread thread1 = new Thread(group1, "thread1");
      Thread thread2 = new Thread(group1, "thread2");
      
      // 해당 스레드 그룹의 일시정지 상태인 스레드 실행 대기 상태로 만든다.
      group1.interrupt();

스레드 상태와 제어 및 동기화

Thread.sleep()

  • 자기 자신에 대해서만 일시정지 상태로 만든다.
    • 특정 스레드 지목이 불가능하다.
  • InterruptedException 예외 처리를 반드시 해줘야 한다.

interrupt()

  • 일시정지 상태인 스레드를 실행대기 상태로 만든다.
    • 특정 스레드 지목이 가능하다.
  • 일시정지 상태인 스레드에 대해서 호출하면, InterruptedException 예외가 발생하며 실행대기 상태로 된다.
    • try-catch에서 예외를 처리해줄 수 있다.
  • isInterrupted() 메소드를 통해, 해당 스레드가 interrupt된 것인지를 파악할 수 있다.
    public class Main {
        public static void main(String[] args) {
            Runnable task = () -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName());
                    } catch (InterruptedException e) {
    										// interrupt가 호출되면, 무한 반복문을 탈출
                        break;
                    }
                }
                System.out.println("task : " + Thread.currentThread().getName());
            };
    
            Thread thread = new Thread(task, "Thread");
            thread.start();
    
            thread.interrupt();
    
            System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
            
        }
    }

join()

  • 정해진 시간동안 지정한 스레드가 작업하는 것을 기다린다.
    • 시간을 지정하지 않는다면, 지정한 스레드의 작업이 끝날 때까지 기다린다.

      public class Main {
          public static void main(String[] args) {
              Runnable task = () -> {
                  try {
                      Thread.sleep(5000); // 5초
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              };
      
              Thread thread = new Thread(task, "thread");
      
              thread.start();
      
              long start = System.currentTimeMillis();
      
              try {
                  thread.join(); // 메인 스레드는 5초간 기다리게 된다.
              } catch (InterruptedException e) {
      						// interrupt()로 메인 스레드를 강제로 깨울 수도 있다.
      						// 그렇기에 예외 처리를 해줘야만 한다.
                  e.printStackTrace();
              }
      
              // thread 의 소요시간인 5000ms 동안 main 쓰레드가 기다렸기 때문에 5000이상이 출력됩니다.
              System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
          }
      }

Thread.yield()

  • 남은 시간을 다음 스레드에게 양보하고 스레드 자신은 실행대기 상태가 된다.
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
								// interrupt를 통해 예외 처리가 되며
								// 다른 스레드에게 리소스가 양보된다.
                Thread.yield();
            }
        };

        Thread thread1 = new Thread(task, "thread1");
        Thread thread2 = new Thread(task, "thread2");

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000); // 메인 스레드는 5초간 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

				// 스레드 1을 실행대기 상태로 변경되며 yield()를 호출
        thread1.interrupt();

    }
}

synchronized

  • 스레드 동기화
  • 임계 영역의 독점을 보장해주는 키워드
    • 임계 영역에는 Lock을 가진 단 하나의 스레드만 출입이 가능하다.
    • 단, 그만큼 실행 시간은 길어진다.

wait()

  • 해당 스레드가 lock을 반납하고 기다리게 만는 메소드
    • lock을 가지고 있지 않은 상태에서 호출하면 에러 발생
    • sleep()랑을 달리, 단순히 기다리는것이 아니라 lock을 반납한다.
  • 실행 중이던 스레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다린다.
    • notify() 호출을 받게 되면 lock을 얻어 진행할 수 있다.
  • 스레드 동기화를 위한 메소드
    • 오직 synchronized 블록 내에서만 호출이 가능

notify(), notifyAll()

  • notify()
    • 해당 객체의 대기실에 있는 모든 스레드 중에서 임의의 스레드만 통지를 받는다.
  • notifyAll()
    • 대기실에 있는 모든 스레드에게 통지한다.
public class Main {
    public static String[] itemList = {
            "MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
    };
    public static AppleStore appleStore = new AppleStore();
    public static final int MAX_ITEM = 5;

    public static void main(String[] args) {

        // 가게 점원
        Runnable StoreClerk = () -> {
                while (true) {
                    int randomItem = (int) (Math.random() * MAX_ITEM);
                    appleStore.restock(itemList[randomItem]);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException ignored) {
                    }
                }
        };

        // 고객
        Runnable Customer = () -> {
                while (true) {
                    try {
                        Thread.sleep(77);
                    } catch (InterruptedException ignored) {
                    }

                    int randomItem = (int) (Math.random() * MAX_ITEM);
                    appleStore.sale(itemList[randomItem]);
                    System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
                }
        };

        new Thread(StoreClerk, "StoreClerk").start();
        new Thread(Customer, "Customer1").start();
        new Thread(Customer, "Customer2").start();

    }
}

class AppleStore {
    private List<String> inventory = new ArrayList<>();

    public void restock(String item) {
        synchronized (this) {
            while (inventory.size() >= Main.MAX_ITEM) {
                System.out.println(Thread.currentThread().getName() + " Waiting!");
                try {
                    wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
                    Thread.sleep(333);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 재입고
            inventory.add(item);
            notify(); // 재입고 되었음을 고객에게 알려주기
            System.out.println("Inventory 현황: " + inventory.toString());
        }
    }

    public synchronized void sale(String itemName) {
        while (inventory.size() == 0) {
            System.out.println(Thread.currentThread().getName() + " Waiting!");
            try {
                wait(); // 재고가 없기 때문에 고객 대기중
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        while (true) {
            // 고객이 주문한 제품이 있는지 확인
            for (int i = 0; i < inventory.size(); i++) {
                if (itemName.equals(inventory.get(i))) {
                    inventory.remove(itemName);
                    notify(); // 제품 하나 팔렸으니 재입고 하라고 알려주기
                    return; // 메서드 종료
                }
            }

            // 고객이 찾는 제품이 없을 경우
            try {
                System.out.println(Thread.currentThread().getName() + " Waiting!");
                wait();
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

Lock, Condition

Lock

  • 스레드가 임계 영역에 들어갈 때 필요한 데이터(객체)
    • 어느 한 스레드가 lock을 가졌다면, 다른 스레드는 그 lock을 가지기 전까진 임계 영역에 들어갈 수 없음
  • Lock의 종류
    • ReentrantLock
      • 재진입이 가능한 lock, 가장 일반적인 배타 lock

      • synchronized 블록과 비슷함

        synchronized(lock){...}
        
        ---
        
        lock.lock();
        ...
        lock.unlock();
    • ReentrantReadWriteLock
      • 읽기에는 공유적이고, 쓰기에는 배타적인 lock
    • StampedLock
      • ReentrantReadWriteLock 에 낙관적인 lock의 기능을 추가

Condition

  • 대기중인 스레드를 구분하기 위한 객체
    • 기존의 notify() 메소드는 어떤 스레드를 깨워야 할 지 특정하질 못했다.

    • Condition을 사용한다면, 특정할 수 있다.

      private ReentrantLock lock = new ReentrantLock();
      
      // lock으로 condition 생성
      private Condition condition1 = lock.newCondition();
      private Condition condition2 = lock.newCondition();
      
      private ArrayList<String> tasks = new ArrayList<>();
      
      // 작업 메서드
      public void addMethod(String task) {
      		lock.lock(); // 임계영역 시작
      
      		try {
      			while(tasks.size() >= MAX_TASK) {
      					String name = Thread.currentThread().getName();
      					System.out.println(name+" is waiting.");
      					try {
      						condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
      						Thread.sleep(500);
      					} catch(InterruptedException e) {}	
      			}
      
      			tasks.add(task);
      			condition2.signal(); // notify();  기다리고 있는 condition2를 깨워줍니다.
      			System.out.println("Tasks:" + tasks.toString());
      		} finally {
      			lock.unlock(); // 임계영역 끝
      		}
      	}
profile
난 Java도 좋고, 다른 것들도 좋아
post-custom-banner

0개의 댓글