자바의 정석(남궁성)을 읽으면서

Thread

싱글 코어에서는 멀티 쓰레드로 하나의 작업을 수행해도 싱글 쓰레드로 작업을 수행할 때 보다 더 시간이 걸린다.

이유는 th1 -> th2 -> th1 -> th2 의 순차 반복일 뿐이여서 오히려 context switching 비용만 추가로 생긴다.

멀티 코어에서는 효과가 있다. th1과 th2가 겹쳐서 수행될 수 있기 때문이다.

(하나의 코어는 하나의 쓰레드를 실행한다는 전제)

(여기서 쓰레드를 tomcat 쓰레드 풀의 쓰레드와 연관지어 생각하지 말자.)


java가 JVM 에서 동작하므로 os에 독립적이라고 하지만 실제로 os 종속적인 부분이 몇 가지 있는데 쓰레드도 그 중 하나이다.

JVM 의 쓰레드 스케쥴러에 의해서 어떤 쓰레드가 얼마동안 실행될 것인지 결정되는 것과 같이 프로세스도 프로세스 스케쥴러에 의해서 순서와 실행 시간이 결정된다. 여기서 매 순간 상황에 따라 프로세스에 할당되는 시간이 다르고 그에 종속적으로 쓰레드에 할당되는 시간 역시 일정하지 않게 된다.


서로 다른 자원을 사용하는 작업의 경우, 멀티 쓰레드가 효율적이다.

 

쓰레드가 입출력을 처리하기 위해 기다리는 것을 I/O Blocking이라 한다.


자바에서 쓰레드의 우선 순위를 정할 수 있는데 멀티 코어에서는 일관된(의미있는) 결과를 기대할 수 없는데

자바는 쓰레드가 우선순위에 따라 어떻게 다르게 처리되어야 하는지에 대해 강제하지 않으므로 쓰레드의 우선순위와 관련된 구현이 JVM마다 다를 수 있기 때문이다.


쓰레드를 그룹으로 관리할 수 있다. 자신이 속한 그룹과 하위는 변경할 수 있지만 다른 그룹의 쓰레드는 변경할 수 없다.

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다. 그룹을 지정하지 않고 생성한 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 그룹에 속하게 된다.

 

자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 그룹에 포함시킨다. 예시로 GC을 수행하는 Finalizer 쓰레드는 system그룹에 속하고 우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 되며 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main쓰레드 그룹에 속하게 된다.


쓰레드가 생성된 후부터 종료될 때까지 가질 수 있는 상태는 다음과 같다.

  • NEW : 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능한 상태
  • BLOCKED : 동기화블럭에 의해서 일시정지된 상태(lock을 획득할 때까지 기다리는 상태)
  • WAITING, TIME_WATING : 작업이 종료되지 않았지만 실행가능하지 않은 상태(시간 정지 이외에 뭐가 있지?), TIME_WATING은 일시정지 시간이 지정된 경를 의미
  • TERMINATED : 쓰레드 작업이 종료된 상태


try {
    th1.sleep(1000);
} catch (InterruptedException e) {}

sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동하므로 th1.sleep을 호출해도 실제 기능은 main(현재)쓰레드에 적용된다.

 

th.interrupt();

class MyThread extends Thread {
    public void run() {
        while(!interrupted()) {
            ....
        }
    }
}

interrup는 쓰레드의 작업을 강제로 종료시키지 못한다. 그저 쓰레드의 interrupted상태를 바꾼다.

그래서 interrupt가 호출되었다면 interrupted()는 true를 반환한다.

 

join() - 다른 쓰레드의 작업을 기다린다.

yield() - 다른 쓰레드에게 양보한다.


같은 자원을 사용하는 작업의 경우, 멀티 쓰레드라면 쓰레드의 동기화를 고려해야한다.

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화' 라고 한다.

 

임계 영역(critical section)과 잠금(lock)의 개념이 있다.

가장 간단한 동기화 방법인 synchronized 키워드를 보자. (이 밖에도 여러가지 locks, atomic 등)

// 메서드 전체를 임계 영역으로 지정
public synchronized void test() {
    ....
}
    
// 특정 블럭을 임계 영역으로 지정
synchronized(객체의 참조변수) {
    ....
}

위와 같은 두 가지 방식이 있다. 이때 참조 변수는 lock을 걸고자하는 객체를 참조하는 것이어야 한다.

두 방법 모두 lock의 획득과 반납이 모두 자동적으로 이루어 진다.

모든 객체는 lock을 하나씩 가지고 있다.

 

메서드 지정이던 블럭 지정이던 lock은 객체에 대해 걸린다. (메서드나 블럭에 걸리는게 아니다.)

 

synchronized로 공유 데이터를 보호하되 특정 쓰레드가 객체의 lock을 오랜 시간 가지지 않는 것도 중요하다.

그래서 wait()와 notify()를 사용한다.

th1가 wait()를 호출하여 lock을 반납하고 해당 지점에서 대기하다가 lock을 획득한 th2가 작업을 끝내고 notify()를 호출하면 th1이 작업을 재개한다.

 

notify()의 문제는 여러 쓰레드가 wait()하고 있을 때, 오래 기다린 쓰레드가 lock을 얻는다는 보장이 없다.

notifyAll()을 사용해 모든 쓰레드에게 통보할 순 있지만 lock은 하나 뿐이다.

 

waiting pool은 객체마다 존재한다.

notifyAll()이 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것은 아니다.

notifyAll()이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당된다.

'Language > Java' 카테고리의 다른 글

Jmeter STOMP test (WebSocket Samplers)  (0) 2024.01.27
java factory method pattern  (0) 2023.09.06
java object equals  (0) 2023.08.08
자바) ArrayList LinkedList 시간 비교 (23-07-06)  (0) 2023.07.13
자바) ArrayList Capacity (23-07-06)  (0) 2023.07.13