Chapter 13. Thread

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 인용.