Chapter 17. 스레드
1. 쓰레드의 이해와 생성
쓰레드의 이해와 Thread 클래스의 상속 쓰레드와 프로세스의 이해 및 관계 • 프로세스는 실행중인 프로그램을 의미한다. • 쓰레드는 프로세스 내에서 별도의 실행흐름을 갖는 대상이다. • 프로세스 내에서 둘 이상의 쓰레드를 생성하는 것이 가능하다. 프로그램이 실행될 때 프로세스에 할당된 메모리, 이 자체를 단순히 프로세스라고 하기도 한다. 사실 쓰레드는 모든 일의 기본 단위이다. main 메소드를 호출하는 것도 프로세스 생성시 함께 생성되는 main 쓰레드를 통해서 이뤄진다.
쓰레드의 생성 실행결과 ThreadUnderstand.java 별도의 쓰레드 생성을 위해서는 별도의 쓰레드 클래스를 정의해 야 한다. 쓰레드 클래스는 Thread를 상속 하는 클래스를 의미한다. 쓰레드의 main 메소드가 run이다! 실행결과 start 메소드가 호출되면 쓰레드 가 생성되고, 생성된 쓰레드는 run 메소드를 호출한다.
쓰레드의 생성을 보인 첫 번째 예제의 의문점1 run은 쓰레드의 생성과 실행을 동시에 명령하는 메소드이다. 쓰레드의 동시 실행은 CPU의 수와 무관하게 이뤄진다.
쓰레드의 생성을 보인 첫 번째 예제의 의문점2 쓰레드의 run 메소드 종료는 쓰레드의 종료로 이어진다.
쓰레드의 생성을 보인 첫 번째 예제의 의문점3 쓰레드라는 표현은 매우 포괄적이다!
쓰레드를 생성하는 두 번째 방법 실행결과 RunnableThread.java join 메소드가 호출되면, 해당 쓰 레드의 종료를 기다리게 된다! 위 예제에서 main 쓰레드가 join 메소드를 호출하지 않았다면, 추가로 생 성된 두 쓰레드가 작업을 완료하기 전에 값을 참조하여 쓰레기 값이 출력 될 수 있다. 실행결과
2. 쓰레드의 특성
쓰레드의 스케줄링과 우선순위 컨트롤 실행결과 쓰레드 스케줄링의 두 가지 기준 우선순위가 높은 쓰레드의 실행을 우선시한다. 우선순위가 동일할 때는 CPU의 할당시간을 나눈다. PriorityTestOne.java 메소드 getPriority의 반환값을 통해서 쓰레드의 우선순위를 확인할 수 있다. 왼쪽의 실행결과에서 보이듯이, 우선순위와 관련해서 별도의 지시를 하지 않으면, 동일한 우선순위의 쓰레드들이 생성된다. 실행결과
우선순위가 다른 쓰레드들의 실행 실행결 과 PriorityTestTwo.java 10 Thread.MAX_PRIORITY는 상수로 10, Thread.NORM_PRIORITY는 상수로 5, Thread.MIN_PRIORITY는 상수로 1 실행결과에서 보이듯이 쓰레드의 실행시간은 우선순위의 비율대로 나뉘지 않는다. 높은 우선순위의 쓰레드가 종료되어야 낮은 우선순위의 쓰레드가 실행된다. 1 실행결 과
낮은 우선순위의 쓰레드 실행 실행결과 PriorityTestThree.java 쓰레드가 CPU의 할당을 필요로 하지 않을 경우, CPU를 다른 쓰레드에게 양보한다. 실행결과 CPU를 양보!
쓰레드의 라이프 사이클 Runnable 상태의 쓰레드만이 스케줄러에 의해 스케줄링 가능하다. 그리고 앞서 보인 sleep, join 메소드의 호출로 인해서 쓰레드는 Blocked 상태가 된다. 한번 종료된 쓰레드는 다시 Runnable 상태가 될 수 없지만, Blocked 상태의 쓰레드는 조건이 성립되면 다시 Runnable 상태가 된다.
쓰레드의 메모리 구성 모든 쓰레드는 스택을 제외한 메소드 영역과 힙을 공유한다. 따라서 이 두 영역을 통해서 데이터 를 주고 받을 수 있다. 스택은 쓰레드 별로 독립적일 수 밖에 없는 이유는, 쓰레드의 실행이 메소드의 호출을 통해서 이 뤄지고, 메소드의 호출을 위해서 사용되는 메모리 공간이 스택이기 때문이다.
쓰레드간 메모리 영역의 공유 예제 실행결과 ThreadHeapMultiAccess.java 위의 예제는 둘 이상의 쓰레드가 메모리 공간에 동시 접근하는 문제를 가지고 있다. 따라서 정상 적이지 못한 실행의 결과가 나올 수도 있다.
3. 동기화(Synchronization)
쓰레드의 메모리 접근방식과 그에 따른 문제점 정상적 연산결과를 보이는 연산의 예 변수 num에 저장된 값을 1씩 증가시키는 두 쓰레 드의 연산의 예 비정상적 연산결과를 보이는 연산의 예 따라서 둘 이상의 쓰레드가 하나의 메모리 공간에 동시 접근하는 것은 문제를 일으킨다.
Thread-safe 합니까? Note that this implementation is not synchronized API 문서에는 해당 클래스의 인스턴스가 둘 이상의 쓰레드가 동시에 접근을 해도 문제가 발생하 지 않는지를 명시하고 있다. 따라서 쓰레드 기반의 프로그래밍을 한다면, 특정 클래스의 사용에 앞서 쓰레드에 안전한지를 확인해야 한다.
쓰레드의 동기화 기법1 : synchronized 기반 동기화 메소드 동기화 메소드의 선언! synchronized 선언으로 인해서 increment 메소드는 쓰레드에 안전한 함수가 된다. synchronized 선언으로 인해서 increment 메소드는 정상적으로 동작한다. 그러나 엄청난 성능의 감소를 동반한다! 특히 위 예제와 같이 빈번함 메소드의 호출은 문제가 될 수 있다.
synchronized 기반 동기화 메소드의 정확한 이해 동기화에 사용되는 인스턴스는 하나이며, 이 인스턴스에는 하나의 열쇠만이 존재한다. 동기화의 대상은 인스턴스이며, 인스턴스의 열쇠를 획득하는 순간 모든 동기화 메소드 에는 타 쓰레드의 접근이 불가능하다. 따라서 메소드 내에서 동기화가 필요한 영역이 매우 제한적이라면 메소드 전부를 synchronized로 선언하는 것은 적절치 않다.
쓰레드의 동기화 기법2 : synchronized 기반 동기화 블록 동기화 블록 기반 동기화 메소드 기반 동기화 블록을 이용하면 동기화의 대상이 되 는 영역을 세밀하게 제한할 수 있다. synchronized(this)에서 this는 동기화의 대상을 알리는 용도 로 사용이 되었다. 즉, 메소드가 호출된 인스턴스 자신의 열쇠 를 대상으로 동기화를 진행하는 문장이다.
동기화 블록의 예 왼쪽의 코드에서 보이듯이 동기화 블록을 이 용하면 동기화의 기준을 다양화할 수 있다. 보다 일반적인 형태, 두 개의 동기화 인스턴스 중 하나는 this로 지정! 왼쪽의 코드에서 보이듯이 동기화 블록을 이 용하면 동기화의 기준을 다양화할 수 있다.
쓰레드 접근순서의 동기화 필요성 본 예제가 논리적으로 실행되려면 NewsWriter 쓰레 드가 먼저 실행되고, 이어서 NewReader 쓰레드가 실 행되어야 한다. 하지만 이를 보장하지 못하는 구조로 구현이 되어 있다.
wait, notify, notifyall에 의한 실행순서 동기화 public final void wait() throws InterruptedException 위의 함수를 호출한 쓰레드는 notify 또는 notifyAll 메소드가 호출될 때까지 블로킹 상태에 놓이게 된다. public final void notify() wait 함수의 호출을 통해서 블로킹 상태에 놓여있는 쓰레드 하나를 깨운다. public final void notifyAll() wait 함수의 호출을 통해서 블로킹 상태에 놓여있는 모든 쓰레드를 깨운다. synchronized(this) { wait(); } 위의 함수들은 왼쪽에서 보이는 바와 같이 한 순간에 하나의 쓰레드만 호출할 수 있도록 동기 화 처리를 해야 한다.
실행순서 동기화 예제 wait과 notifyAll 메소드에 의한 동기화가 진행될 때, 이전 예제에서 달라지는 부분은 쓰레드 클래스 가 아닌 쓰레드에 의해 접근이 이뤄지는 NewsPaper 클래스라는 사실에 주목하기 바란다.
4. 새로운 동기화 방식
synchronized 키워드의 대체 ReentrantLock 인스턴스 를 이용한 동기화 기법 Java Ver 5.0 이후로 제공 된 동기화 방식이다. lock 메소드와 unlock 메소드의 호출을 통해서 동기화 블 록을 구성한다. 보다 안정적인 구현모델, 반드시 unlock 메소드가 호출되는 모델
await, signal, signalAll에 의한 실행순서의 동기화 ReentrantLock 인스턴스 대상으로 newCondition 메소드 호출 시, Condition 인터페 이스를 구현하는 인스턴스의 참조 값 반환! 이 인스턴스를 대상으로 위의 메소드를 호출하여, 쓰레드의 실행순서를 동기화 한다. 위의 메소드의 사용방법을 보이는 예제 ConditionSyncStringReadWrite.java는 코드 양이 많은 관계로 해당 파일을 열어서 참고하기 바란다. 참고로, 앞서 보인 wait, notify 메소드 의 호출을 통한 동기화 방법과 크게 다르지 않다.