Chapter 13. Thread
- 멀티 쓰레딩의 장단점
- CPU의 사용률을 향상시킨다.
- 사용자에 대한 응답성이 향상된다.
- 사용자에 대한 응답성이 향상된다,
- 작업이 분리되어 코드가 간결해 진다.
Context Switching
- 멀티 쓰레드 환경에서 문맥 전환( Context Switching )은 시간이 걸린다.
- 싱글코어와 멀티코어의 같은 작업에 대한 수행시간은 같거나 싱글 코어가 더 빠를수도 있다.
- 문맥 전환시 프로세스의 PCB에 저장되며, CPU의 레지스터 값, 프로세스의 상태, 메모리 관리 정보등을 포함한다.
Priority
쓰레드 우선순위는 쓰레드를 실행하기 전에만 변경할 수 있다.
Thread Group ( 쓰레드 그룹 )
- 쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것
- 쓰레드 그룹은 보안상의 이유로 도입된 개념이다.
- 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.
- 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.
- 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 포함 된다.
- JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다.
- 우리가 생성하는 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹에 속하게 된다.
Daemon Thread ( 데몬 쓰레드 )
- 데몬 쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.
- 보조 역할이므로 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 일반 쓰레드의 보조 역할 이므로 일반 쓰레드가 다 죽으면 존재 이유가 없다.
- 이 점을 제외 하고는 다른점이 없다.
- 가비지 컬렉터, 워드 프로세서의 자동 저장, 화면 자동 갱신
쓰레드의 동기화
- Java.util.concurrent.locks, java.util.concurrent.atomic 패키지를 통해 다양한 방식으로 동기화를 구현할 수 있도록 지원, since jdk1.5
- 임계영역은 멀티 쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다 synchronized 블럭으로 임계영역을 최소화 해서 보다 효율적인 프로그램이 되도록 노력해야 한다.
- NotifyAll()은 모든 객체의 wating이 깨워지는것은 아니다. 객체마다 wating pool이 존재하기 때문이다.
Lock과 Condition을 이용한 동기화
Java,util.concurrent.locks 패키지가 제공하는 lock클래스를 이용하기.
synchronized 블럭은 자동적으로 잠기고 풀린다. 블럭 내에서 예외가 발생해도 lock은 자동적으로 풀리기 때문에 편리하다 하지만 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 불편할때 java.util,concurrent.locks를 이용한다.
Reentrantlock : 재진입이 가능한 락, 가장 일반적인 베타 lock
Reentrantreadwritelock : 읽기에는 공유적이고 쓰기에는 베타적인 lock
stampedLock : reentrantreadwritelock에 낙관적인 lock의 기능을 추가
ReentrantReadWriteLock : 읽기 Lock은 동시에 읽기 가능. 읽기 Lock이 걸려있는 상태에서 Write 진입 불가, Write 락 중에서도 Read 불가.
StampedLock : 읽기와 쓰기를 위한 낙관적인 Lock. 쓰기와 읽기가 충돌할 떄만 쓰기가 끝난 후에 읽기 Lock을 건다.
lock.lock();
Try {
// 임계 영역
} finally {
lock.unlock();
}
일반적으로 finally로 묶어준다. tryLock(); 은 lock을 얻으려고 기다리지 않느다. 지정된 시간 만큼만 기다리고 얻으면 true 아니면 false
Volatile
- 멀티 코어 프로세서에서는 코어 마다 별도의 캐시를 가지고 있다.
- 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다.
- 다시 같은 값을 읽어올 떄는 먼저 캐시에 있는지 확인하고 없을때만 메모리에서 읽어온다.
- 변수 앖에 volatile boolean stopFlag = false; 를 붙이면 캐시가 아닌 메모리에서 값을 읽어오기 때문에 캐시와 메모리간 값의 불일치가 해결된다.##
- But 도중에 메모리에 저장된 값이 다른 경우가 발생한다. 그래서 변수 stopped의 값이 바뀌었는데도 쓰레드가 멈추지 않고 계속 실행되는 것이다.
- volatile 대신 synchronized 블럭을 사용해도 같은 효가를 얻을수 있다. 쓰레드가 synchronized 블럭으로 들어갈때와 나올때 캐시와 메모리간의 동기화가 이루어 지기 때문에 값의 불일치가 해소된다.
volatile로 long과 double를 원자화
- JVM은 데이터를 4 바이트(=32bit) 단위로 처리하기 때문에 인트와 인트보다 작은 타입들은 한번에 읽거나 쓰는것이 가능하다.
- 이는 하나의 명령어로 처리 가능하고 더이상 나눌수 없는 최소의 작업 단위 임으로 작업 중간에 다른 쓰레드가 끼어들 틈이 없다.
- 그러나 크기가 8byte인 long과 double 타입의 변수는 하나의 명령어로 값을 읽거나 쓸수 없다. 따라서 다른 쓰레드가 끼어들 여지가 있다.
- 따라서 volatile long sharedVal; volatile double sharedVal;로 8byte를 원자화한다.
- 하지만 volatile은 변수의 읽기와 쓰기를 원자화 할뿐 동기화 하는것은 아니다.
Fork & join 프레임 워크
코어가 늘어나는 CPU가 발전함에 따라 멀티코어를 잘 활용할 수 있는 멀티 쓰레드 프로그래밍이 중요해지고 있다.
JDK 1.7부터 fork&join 프레임 웍이 추가되었고 이 프레임 웍은 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어 준다.
RecursiveAction : 반환값이 없는 작업을 구현할때, RecursiveTask : 반환값이 있는 작업을 구현할때
ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(from, to);
Long result = pool.invoke(task);
ForkJoinPool은 해당 프레임웍에서 제공하는 쓰레드 풀로 지정된 쓰레드를 생성해 미리 만들어놓고 반복해서 재사용할 수 있게 한다.
쓰레드를 반복해서 생성하지 않아도 된다는 장점과, 너무 많은 쓰레드가 생성되어 성능이 저하되는 것을 막아준다는 장점이 있다.
쓰레드 풀은 쓰레드가 수행해야하는 작업이 담긴 큐를 제공하며 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리한다.
쓰레드 풀은 기본적으로 코어의 개수와 동일한 개수의 쓰레드를 생성한다.
fork()는 비동기 메서드, join()은 동기 메서드이다.
출처 : 남궁성, 『 자바의 정석 3/E』, 도우출판(2016.1.27), chapter 13 인용.