15. 스레드와 락
- 스레드로 알고리즘을 구현하라는 문제를 출제하는 일은 흔하지 않음.
- 하지만 스레드, 특히 교착상태(deadlock)에 대한 일반적 이해도를 평가하기 위한문제느 어떤 회사에서도 상대적으로 자주 출제하는 편임.
자바의 스레드
- 자바의 모든 스레드는 java.lang.Thread 클래스 객체에 의해 생성되고 제어됨.
- 독립적인 응용 프로그램이 실행될 때, main() 메서드를 실행하기 위한 하나의 사용자 스레드(user thread)가 자동으로 만들어짐.
- 이를 주 스레드(main thread) 라고 함.
- 자바에서 스레드를 구현하는 방법은 2가지가 있음.
- java.lang.Runnable 인터페이스 구현하기
- java.lang.Thread 클래스 상속받기
Runnable 인터페이스를 구현하는 방법
public interface Runnable {
void run();
}
- 이 인터페이스를 사용해 스레드를 만들고 사용하려면 다음의 과정을 거쳐야 함.
- Runnable 인터페이스를 구현하는 클래스를 만든다.
- Thread 타입의 객체를 만들 때, Thread의 생성자에 Runnable 객체를 인자로 넘긴다.
- 이전 단계에서 생성한 Thread 객체의 start() 메서드를 호출한다.
Thread 클래스 상속
- Thread 클래스를 상속받아서 스레드를 만들 수도 있음.
- 거의 항상 run() 메서드를 오버라이드해야 함.
- 하위 클래스의 생성자는 하위 클래스의 생성자를 명시적으로 호출해야 함.
Thread 상속 vs. Runnable 인터페이스 구현
- 스레드 생성할 때, Runnable 인터페이스를 구현하는 것이 Thread 상속받는 것보다 선호됨.
- 자바는 다중 상속을 지원하지 않음. 따라서 Thread 클래스를 상속하게 도미ㅕㄴ 하위 클래스는 다른 클래스를 상속할 수 없음.
- Thread 클래스의 모든 것을 상속받는 것이 너무 부담될 수 있음.
동기화와 락
- 어떤 프로세스 안에서 생성된 스레드들은 같은 메모리 공간을 공유함.
- 스레드가 서로 데이터를 공유할 수 있다는 점은 장점이긴 하지만, 두 스레드가 같은 자원을 동시에 변경하는 경우 문제가 됨.
- 자바는 공유 자원에 대한 접근을 제어하기 위한 동기화 방법을 제공함.
- synchronized와 Lock 이라는 키워드는 동기화 구현을 위한 기본이 됨.
동기화된 메서드
- 통상적으로 synchronized 키워드를 사용할 떄는 공유 자원에 대한 접근을 제어한다.
- 이 키워드는 메서드에 적용할 수도 있고, 특정한 코드 블록에 적용할 수도 있음.
- 여러 스레드가 같은 객체를 동시에 실행하는 것 또한 방지해줌.
- 정적 메서드(static method) 는 클래스 락(class lock)에 의해 동기화 됨.
- 같은 클래스에 있는 동기화된 정적 메서드는 두 스레드에서 동시에 실행될 수 없음.
동기화된 블록
- 특정한 코드 블록을 동기화할 수도 있음.
- 메서드 동기화하는 것과 아주 비슷하게 동작함.
락
- 좀 더 세밀하게 동기화를 제어하고 싶을 때는 락(lock)을 사용함.
- 락(모니터(monitor)라고도 함)을 공유 자원에 붙이면 해당 자원에 대한 접근을 동기화할 수 있음.
- 스레드가 해당 자원을 접근하려면 우선 그 자원에 붙어 있는 락을 획득(acquire)해야 함.
- 특정 시점에 락을 쥐고 있을 수 있는 스레드는 하나뿐임.
- 따라서, 해당 공유자원은 한 번에 한 스레드만이 사용할 수 있음.
교착상태와 교착상태 방지
- 교착상태(deadlock)란 첫 번째 스레드는 두 번쨰 스레드가 들고 있는 객체의 락이 풀리기를 기다리고 있고, 두 번째 스레드 역시 첫 번쨰 스레드가 들고 있는 객체의 락이 풀리기를 기다리는 상황을 일컫는다.
- 모든 스레드가 락이 풀리기를 기다리고 있기 떄문에 무한 대기 상태에 빠짐.
- 이런 스레드를 교착 상태에 빠졌다고 함.
- 교착 상태가 발생하려면 다음의 네 가지 조건이 모두 충족되어야 함.
- 상호 배제(mutual exclusion)
- 한 번에 한 프로세스만 공유 자원을 사용할 수 있음.
- 자원의 양이 제한되어 있더라도 교착 상태는 발생할 수 있음.
- 들고 기다리기(hold and wait)
- 공유 자원에 대한 접근 권한을 갖고 있는 프로세스가, 그 접근 권한을 양보하지 않은 상태에서 다른 자원에 대한 접근 권한을 요구할 수 있음.
- 선취(preemption) 불가능
- 한 프로세스가 다른 프로세스의 자원 접근 권한을 강제로 취소할 수 없음.
- 대기 상태의 사이클(circular wait)
- 두 개 이상의 프로세스가 자원 접근을 기다리는데, 그 관계에 사이클(cycle)이 존재함.
- 교착 상태를 방지하기 위해, 이 조건들 가운데 하나를 제거하면 됨.
- 이 조건 가운데 상당수는 만족되기 어려운 것이여서 까다로움.
- 공유 자원중 많은 경우, 한 번에 한 프로세스만 사용할 수 있기 떄문에, 1번 조건은 제거하기 어려움.
- 대부분의 교착상태 방지 알고리즘은 4번 조건, 대기 상태의 사이클이 발생하는 일을 막는 데 초점이 맞춰져 있음.
면접 문제
15.1 프로세스 vs. 스레드
15.2 문맥 전환
- 문맥 전환(context switch)에 소요되는 시간을 측정하려면 어떻게 해야 할까?
15.3 철학자의 만찬
- 유명한 철학자의 만찬 문제(dining philosophers problem)를 떠올려 보자. 철학자들은 원형 테이블에 앉아 있고 그들 사이에젓가락 한 짝이 놓여 있다. 음식을 먹으려면 젓가락 두 짝이 전부 필요한데, 철학자들은 언제나 오른쪽 젓가락을 집기 전에 왼쪽 젓가락을 먼저 집는다. 모든 철학자들이 왼쪽에 있는 젓가락을 동시에 집으려고 하면, 교착상태에 빠질 수 있다. 철학자들의 만찬 문제를 시뮬레이션하는 프로그램을 작성하라. 단, 스레드와 락을 사용하여 이 프로그램이 교착상태에 빠지지 않도록 하라.
15.4 교착상태 없는 클래스
- 교착상태에 빠지지 않는 경우에만 락을 제공해주는 클래스를 설계해 보라.
15.5 순서대로 호출
public class Foo {
public Foo() { ... }
public void first() { ... }
public void second() { ... }
public void third() { ... }
}
- Foo 인스턴스 하나를 서로 다른 세 스레드에 전달한다. threadA는 first를 호출할 것이고, threadB는 second를 호출할 것이며, threadC는 third를 호출할 것이다. first가 second보다 먼저 호출되고, second가 third보다 먼저 호출되도록 보장하는 메커니즘을 설계하라.
15.6 동기화된 메서드
- 동기화된 메서드 A와 일반 메서드 B가 구현된 클래스가 있다. 같은 프로그램에서 실행되는 스레드가 두 개 존재할 때 A를 동시에 실행할 수 있는가? A와 B는 동시에 실행될 수 있는가?