Java 2022. 11. 11. 01:26

Java의 정석 13장 리뷰

  • 쓰레드를 구현할 때는 run() 메서드를, 실행할 때는 start() 메서드를 호출한다. (p.726)
    • 쓰레드의 호출 스택을 별도로 생성하기 위해서이다.
  • 쓰레드 그룹(thread group) (p.741)
    • 보안 상 이유로 도입된, 쓰레드를 그룹으로 관리하는 클래스이다.
      • 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만, 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.
    • 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하며, 기본적으로는 부모 쓰레드와 같은 쓰레드 그룹에 속한다.
    • JVM은 기본적으로 system과 main 쓰레드 그룹을 생성한다.
    • 다음과 같이 참조 변수 없이 쓰레드를 생성하여 바로 실행시켜도, 이 쓰레드의 참조가 ThreadGroup에 저장되어 있기 때문에 GC 대상이 되지 않는다.
      public static void main(String[] args) throws Exception {
      		ThreadGroup grp1 = new ThreadGroup("Group1");
      		
      		new Thread(grp1, () -> {
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {}
          }, "th1").start();
      }
  • 관련 메서드
    • getAllStackTraces()
      • 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.
    • Thread.sleep()
      • sleep() 메서드는 static으로 선언되어 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에, 다른 쓰레드 인스턴스 메서드를 호출하는 코드(th1.sleep(1000) 등)를 작성해도 해당 쓰레드가 아닌 현재 쓰레드가 WAITING 상태로 들어간다.
    • isInterrupted()
      • interrupted() 메서드와 달리, isInterrupted() 메서드는 쓰레드의 interrupted 상태를 false로 초기화하지 않는다.

       

       

    • suspend(), resume(), stop() (p.755)
      • suspend() 메서드는 sleep() 메서드처럼 쓰레드를 멈추게 하고, resume() 메서드를 통해 실행대기 상태로 돌릴 수 있다.
      • stop() 메서드는 호출 즉시 쓰레드를 종료시킨다.
      • 위의 메서드들은 데드락을 일으키기 쉽게 작성되어 있어서, deprecated 되었다.
    • yield() (p.760)
      • 쓰레드의 남은 할당 시간을 포기하고 다시 실행대기 상태로 돌아가는 메서드이다.
      • while 무한루프 + Thread.sleep() + Thread.yield() 조합으로 애플리케이션의 응답 속도를 높일 수 있다. (busy-waiting 방지)
        • (참고) suspend()stop() 메서드 호출 시에 interrupt() 메서드를 사용하면 Waiting 상태에서 바로 깨어나서 정지시키는 기법으로도 응답 속도를 높일 수 있다.
  • InterruptedException (p.754)
    • Thread.sleep() 메서드 실행 중(Waiting 상태) interrupt 신호를 받으면, 해당 메서드에서 InterruptedException이 발생하고 interrupted 상태는 false로 자동 초기화된다. 따라서 해당 코드 바깥의 try ~ catch 문까지 interrupt 신호가 전달되지 않는다.
  • Concurrency
    • synchronized 블럭
      • 블럭이나 메서드에 synchronized 키워드를 붙이면, 내부 코드에 접근 시 자동으로 임계 영역이 설정된다.
        synchronized(객체의 참조변수) {
            // Critical Section
        }
    • wait(), notify()
      • synchronized 블럭은 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간 있을 수 있기 때문에, 이를 보완하고자 나온 메서드가 위의 두 가지이다.
      • 두 메서드는 Object 클래스에 정의되어 있다.
      • notifyAll() : 기다리는 모든 쓰레드에게 통보한다. 단, lock을 얻을 수 있는 건 하나의 쓰레드 뿐이다.
    • Lock
      • java.util.concurrent.locks 패키지(JDK 1.5 이상)의 lock 클래스들을 이용할 수도 있다.
      • ReentrantLock
        • 가장 일반적인 Lock이다.
        • 생성자에 ReentrantLock(boolean fair) 와 같이 fait 변수를 줄 수 있고, 이 값이 true면 가장 오래 기다린 쓰레드가 Lock을 획득하도록 한다. 하지만 이 과정에서 성능이 떨어지며, 대부분의 경우 공정함보다는 성능을 선택한다.
        • tryLock() 메서드는, 다른 쓰레드에 의해 Lock이 걸려있으면 기다리지 않거나 인자로 받은 시간만큼만 기다리고 Lock을 얻지 못하면 false를 반환한다. 당연히 얻으면 true를 반환한다.
      • ReentrantReadWriteLock
        • 읽기 Lock이 걸려있을 경우, 다른 쓰레드가 읽기 Lock을 중복해서 걸고 데이터를 읽을 수 있다. 하지만 읽기 Lock과 쓰기 Lock을 동시에 걸 수는 없다.
      • StampedLock
        • 읽기, 쓰기 Lock 외에 낙관적 읽기 Lock(optimistic reading lock)을 추가한 것이다. 읽기 Lock이 풀리는 것을, 쓰기 Lock이 기다리는 걸 방지하려고 낙관적 읽기 Lock을 만든 것이다.
        • 낙관적 읽기 Lock은 쓰기 Lock에 의해 바로 풀린다.
        • 즉, 쓰기와 읽기 충돌 시 쓰기가 끝난 후에 읽기 Lock을 건다.

       

    • Condition
      • 쓰레드가 원하는 Lock의 종류를 구분하기 위한 waiting pool이다.
      • Object 클래스의 wait(), notify() 메서드 대신, Condition의 await(), signal() 메서드를 사용하면 된다.
  • volatile (p.786)
    • 코어에서 변수의 값을 읽을 때, 캐시가 아닌 메모리에서 읽어오도록 강제하는 키워드로 사용한다.
      • C, C++의 volatile 키워드와 문맥이 다른 것 같다. (C 계열에서는 최적화 방지 키워드로 사용)
  • fork & join (p.788)
    • JDK 1.7부터 추가된, 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하도록 하는 기법이다.
    • RecursiveAction : 반환 값이 없는 작업 구현 시 사용
    • RecursiveTask : 반환 값이 있는 작업 구현 시 사용