본문 바로가기
  • soobinhand의 기술 블로그
도서/자바 프로그래밍 언어 - James Gosling

[자바 프로그래밍 언어] 14장 스레드

by soobinhand 2022. 1. 3.
728x90

14.1     스레드 생성

  • 컴퓨터에서 한 번에 하나씩 순서대로 실행되는 것을 스레드라고 한다. 단일 스레드 프로그래밍 모델은 대부분의 프로그래머들이 사용하는 방식이다.
  • 은행 직원이라고 볼 수 있는 스레드는 다른 스레드와는 독립적으로 업무를 수행할 수 있다. 그리고 두 명의 은행 직원이 같은 서류 캐비닛을 사용할 수 있는 것처럼 스레드도 객체를 서로 공유할 수 있다. 이를 멀티 스레드라고 한다.
  • 스레드를 생성하기 위해서는 가장 먼저 Thread 객체를 생성해야 한다.
  • Thread worker = new Thread();
  • start 메소드는 Thread 객체의 데이터를 기반으로 새로운 스레드를 만든 후, 이를 반환한다. 그러면 가상 머신은 스레드의 run 메소드를 호출하여 스레드를 동작시킨다. start 메소드는 스레드마다 오직 한 번만 호출 가능.
  • 명시적으로 어떤 스레드를 생성하지 않았더라도 항상 실행 중인 객체는 존재한다. 그것이 바로 main 메소드다.

14.2     Runnable 사용

  • 스레드로 수행되어야 하는 작업은 run 메소드에 작성되어야 한다. 
  • Thread 클래스 자체는 Runnable 인터페이스를 구현하고 있다.
  • 특정 조작을 위해 Thread 클래스를 확장할 수 있지만 이는 대부분의 경우에 그리 좋은 방법이 아니다. 왜냐하면 클래스 확장은 하나 밖에 못하기 때문에 이미 Thread 클래스를 확장했다면 다른 클래스는 확장할 수 없다. 게다가 Runnable 인터페이스만을 구현해도 되는데 굳이 Thread 클래스를 확장하면 필요 이상의 부하가 발생할 수 있다.
  • Runnable 인터페이스를 구현하는 것은 매우 쉽다. Runnable 객체를 Thread 생성자에 전달하는 것으로 Runnable 객체를 실행할 수 있다.

14.3     동기화

  • 멀티 스레드에서는 락을 얻는 경우에만 해당 객체의 사용을 허용하게 하여 문제를 해결할 수 있다. 객체를 사용해야 하는 스레드는 객체의 락을 얻어야만 한다. 그래서 객체의 락을 얻지 못한 스레드는 락을 얻을 수 있을 때까지 해당 객체에 접근할 수 없다. 이렇게 멀티 스레드를 구현하면 스레드들이 서로 간섭하여 데이터를 이상하게 변경하는 일은 발생하지 않는다.
  • 멀티 스레드 환경에서 간섭으로부터 보호되어야 하는 객체의 클래스는 synchronized로 선언된 적당한 메소드를 가지고 있어야 한다.
  • 스레드가 객체의 동기화된 메소드를 호출했을 때, 이 스레드는 객체의 락을 획득한 후 메소드 몸체를 실행한다. 그리고 몸체를 다 실행한 후에 락을 푼다. 같은 객체 상에서 synchronized 메소드를 호출하는 다른 스레드는 락이 풀릴 때까지 대기해야 한다.
  • 스레드 별로 락을 소유한다. 그래서 같은 객체일 경우, 동기화된 메소드 내에 작성된 다른 동기화된 메소드는 대기하지 않고 동작할 것이다.
  • 락은 동기화된 메소드가 종료되자마자 풀린다.
  • 생성자는 synchronized로 선언할 필요가 없다. 왜냐하면 생성자는 객체가 생성될 때만 실행되며, 생성은 한 개의 스레드에서만 일어날 수 있기 때문이다. 문법적으로도 생성자에는 synchronized를 선언할 수 없다.
  • 필드에 접근할 때는 동기화된 메소드를 통해서만 이뤄져야 한다. 이렇게 메소드를 사용하면 데이터 접근을 동기화할 수 있다. 하지만 필드가 public이나 protected로 선언되어 메소드를 통하지 않고 바로 접근할 수 있는 경우에는 동기화할 수 있는 방법이 없다.
  • 스레드 순서는 무작위라 원하는 순서로 작업이 처리되길 바란다면 스레드의 순서를 정해줘야 함.
  • Thread.holdsLock 메소드는 락이 필요할 때 락을 사용할 수 있는지를 확인하기 위한 용도로 사용된다.
  • synchronized 문은 현재 객체뿐만 아니라 어떤 객체의 락이라도 획득할 수 있으며 해당 코드를 동기화하여 실행할 수 있다. synchronized 문은 락을 획득할 객체와 락을 획득한 후에 실행될 문장 두 부분으로 구성된다.
  • synchronized 문의 일반적인 형식은 다음과 같다.
synchronized (expr){
	statements
}
  • 변경될 수 있는 배열은 수정될 수 있는 요소를 가지고 있기 때문에 synchronized 문의 객체로 지정하여 동기화한다. 이제 반복문이 처리되는 동안에는 이 배열의 값은 동기화되어 있는 다른 코드에 의해 변경되지 않는다고 확신할 수 있다.
  • synchronized 문은 synchronized 메소드보다 많은 장점을 가진다. 
    • 문은 동기화 영역을 메소드보다 더 작게 정의할 수 있다.
    • 문은 this 외의 다른 객체도 동기화할 수 있다. 동기화를 좀 더 세분화하여 클래스의 동시성 수준을 증가시키고 싶을 때, 문을 사용하면 클래스의 서로 다른 메소드 그룹들이 이 클래스의 서로 다른 데이터 상에서 동작하게 할 수 있다.
  • 내부 객체의 락을 획득하는 것이 외부 객체의 락에는 영향을 주지 않으며, 마찬가지로 외부 객체의 락을 획득하는 것도 내부 객체의 락에는 아무런 영향을 주지 않는다.
  • 만약 정적 동기화 메소드에서 사용하고 있는 락을 synchronized 문에서 사용해야 한다면 클래스 리터럴을 사용해야 한다.
  • 가장 단순한 규칙은 정적 데이터 접근 보호를 위해 정적 데이터가 선언된 클래스의 Class 객체의 락을 사용해야 한다는 것이다.
  • 공유된 객체를 위한 동기화 메소드는 해당 객체에 만드는 것이 일반적으로 더 나은 방법이다. 이렇게 하면 클라이언트가 동기화하지 않고 객체를 사용하는 것을 막을 수 있다. 이러한 방법으로 동기화하는 것을 서버측 동기화라고 부르며 객체 자체의 행위를 객체에 작성하는 객체 지향 관점의 확장이라고 할 수 있다.
  • 유연성 때문에 시스템 설계 시 인터페이스를 선호한다.

14.4     wait, notifyAll, notify

  • 스레드들이 서로 통신할 수 있는 방법도 필요하다. 이를 위해서 자바에는 어떤 상태가 발생할 때까지 한 개의 스레드를 기다리게 하는 wait 메소드와 해당 상태에 만족하는 어떤 일이 발생했다는 것을 기다리고 있는 스레드에게 알려주는 통지 메소드인 notifyAll과 notify 메소드가 준비되어 있다.
  • notifyAll 메소드를 사용하면 대기하고 있는 스레드를 모두 깨우지만 notify는 오직 스레드 한 개만 깨운다.
  • notify는 오직 다음과 같은 상황에 적용되어야 효과적이다.
    • 스레드들이 모두 동일한 조건을 기다리는 경우
    • 조건을 만족하는 스레드 한 개만이 깨어나야 하는 경우
    • 모든 가능한 서브 클래스들이 협약상으로 올바른 경우

14.5     대기와 통지의 세부 사항

  • wait, notify 메소드들의 세부 사항임

14.6     스레드 스케줄링

  • 스레드는 중요도를 가질 수 있다. 작업의 중요도를 반영하기 위해서는 각각의 스레드가 우선순위를 가지고 실행되어야 한다. 하지만 낮은 우선순위의 스레드에는 기아 현상을 막기 위해 우선순위 증가가 적용될 수도 있다.
  • 시분할 스케줄링은 한 스레드가 선점할 수 있는 시간을 제한한다.

14.7     교착상태

  • 두 개의 스레드와 락을 가진 두 개의 객체가 있다면 교착상태가 될 가능성이 항상 있다. 교착상태는 어떤 스레드가 두 객체 중 하나의 락을 소유하고 있으면서 다른 객체의 락을 기다리고 있는 상태를 말한다. 
  • 런닝맨의 이름표 떼기라고 생각해도 좋을 듯? 서로 뒤돌기를 바라지만 그 상태는 영원할 수 있음.
  • 교착상태를 해결하기 위해서는 설계를 변경해야 한다. 이를 위한 일반적인 방법은 자원 순서화를 사용하는 것이다. 자원 순서화를 사용하면 락이 획득해야 하는 모든 객체에 순서를 지정해서 항상 순서대로 락을 획득하게 할 수 있다. 이렇게 하면 두 스레드가 한 개의 락을 서로 소유하거나 다른 스레드가 소유한 락을 소유하려고 시도하는 것이 불가능하다.

14.8     스레드 실행 종료

  • 시작된 스레드는 생존 상태이기 때문에 이 스레드에서 isAlive 메소드를 실행하면 true가 반환된다. 생존 상태에 있는 스레드는 다음과 같은 세 경우 중 하나로 종료될 수 있다.
    • run 메소드가 정상적으로 반환되는 경우
    • run 메소드가 비정상적으로 종료되는 경우
    • 애플리케이션이 종료되는 경우
  • run 메소드를 반환하는 것은 스레드를 종료하는 가장 일반적인 방법이다. 모든 스레드는 작업을 수행하며 작업이 끝났을 때 스레드도 종료되어야 한다.
  • 인터럽트로 스레드를 취소할 수 있기 때문에 이를 감시하고 응답하며 인터럽트될 수 있도록 작성해야 한다.
  • 스레드를 인터럽트한다는 것은 실행을 중지했으면 한다고 알려주는 것이지 스레드를 강제로 종료하는 것은 아니다. 또한 휴면상태나 대기 상태에 있는 스레드만을 인터럽트할 수 있다.
  • 인터럽트 메커니즘은 코드들이 서로 협력하여 멀티 스레드를 효율적으로 동작할 수 있게 해주는 도구이다.
  • join 메소드를 사용하면 어떤 스레드를 다른 스레드가 종료될 때까지 기다리게 할 수 있다.

14.9     애플리케이션 실행 종료

  • 스레드에는 두 종류, 즉 사용자 스레드와 데몬 스레드가 있다. 사용자 스레드는 애플리케이션을 실행하기 위해 존재하며 데몬 스레드는 애플리케이션을 지원하기 위해 임시적으로 존재한다. 마지막 사용자 스레드가 종료되면 데몬 스레드는 모두 종료되며 애플리케이션은 종료된다.
  • 모든 런타임 시스템은 최초 스레드가 다른 스레드를 생성하고 종료할 수 있도록 설계되어 있으며 이렇게 생성된 스레드는 실제로 작업을 처리한다. 만약 최초 스레드가 종료되었을 때 애플리케이션도 종료되게 하고 싶다면 생성한 모든 스레드를 데몬 스레드로 만들어야 한다.

14.10     메모리 모델 : 동기화와 volatile

  • 여러 스레드가 공유하고 있으면서 변경될 수 있는 값은 항상 동기화를 해서 간섭이 일어나지 못하게 해야 한다. 하지만 동기화를 하면 비용 문제가 발생하므로 항상 간섭을 막을 필요는 없다.
  • 메모리 접근을 순서화하는 방법과 가시성을 언제 보장해야 하는지를 정의하는 규칙을 자바 프로그래밍 언어의 메모리 모델이라고 한다. 스레드가 읽는 변수들의 값은 메모리 모델이 결정한다. 메모리 모델은 변수를 읽었을 때 반환할 수 있는 허용된 값들을 정의한다.
  • 쓰여진 값이 메모리 모델이 읽는 것을 허용하는 유일한 값이 되어야 한다. 이렇게 하기 위해서는 변수의 값을 쓰고 읽는 것을 동기화해야만 한다.
  • 스레드가 수행하는 특정 행위를 동기화 행위라고 하며 이 행위들이 실행되는 순서를 프로그램의 동기화 순서라고 한다. 동기화 순서는 항상 프로그램 순서와 일치한다. 동기화 행위는 변수를 읽거나 변수에 쓰는 작업을 동기화 한다.
  • volatile 변수를 사용하면 변수를 읽을 때마다 가장 최근에 쓰여진 값을 반환하게 할 수 있다. 필드는 volatile 제한자로 선언될 수 있다. volatile 변수에 쓰는 것은 이 변수를 읽는 행위를 모두 동기화한다.
  • 변수를 volatile로 만들면 읽고 쓰는 작업을 원자적으로 동작하게 해준다. 이것은 long이나 double 변수가 원자적으로 동작할 수 있게 해준다.
  • 동기화 블록과 동기화 메소드 그리고 volatile 변수를 사용하면 변수를 읽고 쓰는 작업을 안전하게 할 수 있다.
  • 스레드의 행위가 프로그램 순서를 따르고 메모리 모델이 읽을 수 있게 허용한 모든 값들이 제공되었다면 명령문과 메모리 접근의 실제 실행 순서는 어떤 순서라도 될 수 있다.

14.11     스레드 관리, 보안, 스레드 그룹

  • 멀티 스레드 프로그램을 작성할 때 관련된 스레드들을 하나의 그룹으로 만들고 이 그룹의 스레드들을 한 개의 단위로 관리하는 것이 유용하다.
  • 그룹의 모든 스레드는 인터럽트하거나 모든 스레드를 최고 우선순위로 지정하는 등의 작업을 한 개 단위로 처리할 수 있다.
  • 일반적으로 보안에 민감한 메소드들은 항상 처리되기 전에 보안 관리자로 검사해야 한다. 만약 어떤 작업이 보안 정책에 의해서 강제로 금지된다면 해당 메소드는 SecurityException을 발생시킬 것이다.
  • 스레드 그룹은 새로운 스레드 생성 시 스레드 생성자에 원하는 스레드 그룹을 넘겨주는 것으로 지정할 수 있다.
  • 스레드와 연관된 ThreadGroup 객체는 스레드가 생성된 이후에는 변경할 수 없다.
  • 데몬 스레드 그룹은 다른 스레드를 포함하고 있지 않을 때는 자동으로 파기된다.
  • 스레드 그룹은 포함된 스레드들의 우선순위의 상위 제한을 설정할 수 있다.

14.12     스레드와 예외

  • 예외는 항상 어떤 활동을 하는 특정 스레드에서 발생한다.
  • 예외가 발생 시점에 잡히지 않는다면 run 메소드는 비정상적으로 종료된다. 이를 미처리 예외라고 한다.
  • 시스템에서는 Thread 클래스의 정적 메소드 setDefaultUncaughtExceptionHandler 로 기본 조정자를 설정할 수 있다. 보안 관리자는 애플리케이션이 이를 설정할 권한이 있는지를 검사한다.
  • stop 메소드는 스레드를 강제로 종료시키기 위한 목적으로 작성되었다.
  • 살아있는 스레드에게는 현재 실행 중인 스택 추적 정보를 요청할 수 있다.
  • Thread 클래스의 정적 메소드 getAllStackTraces를 사용하면 시스템에서 모든 스레드에 대한 스택 정보를 추적할 수 있다.
  • 스택 추적 정보는 일반적으로 디버깅이나 모니터링 목적으로 사용한다.

14.13     ThreadLocal 변수

  • ThreadLocal 클래스는 스레드마다 독립적인 값을 가질 수 있는 단일 변수를 보유할 수 있게 해준다.
  • ThreadLocal 객체는 사용하기에 위험한 도구이다. 그래서 사용할 스레드 모델을 완전히 이해하고 있을 때만 사용해야 한다.

14.14     스레드 디버깅

  • Thread 클래스에는 멀티 스레드 애플리케이션을 디버깅할 수 있는 메소드들이 준비되어 있다.
728x90

댓글