07. 쓰레드는 개발자라면 알아두는 것이 좋아요
쓰레드 (Thread)
- JVM이 시작되면 자바 프로세스가 시작된다.
- java 명령어로 클래스를 실행시키면 자바 프로세스가 실행되고 main() 메소드가 실행되면서 하나의 쓰레드가 시작된다.
- 많은 쓰레드가 필요한 경우 main() 메소드에서 쓰레드를 생성해주면 된다.
- 하나의 프로세스 내에서 여러 개의 쓰레드가 수행된다.
- 하나의 프로세스 내에 하나 이상의 쓰레드가 수행된다.
- 하나의 쓰레드를 여러 프로세스가 공유하지는 못한다.
- Tomcat과 같은 WAS(Web Application Server)도 main() 메소드에서 생성한 쓰레드들이 수행되는 것
- 아무런 쓰레드를 별도로 생성하지 않아도 JVM을 관리하는 여러 쓰레드가 존재한다.
- 자바의 쓰레기 객체를 청소하는 GC 관련 쓰레드 등...
- 쓰레드라는 개념이 존재하는 이유는 뭘까?
- 프로세스 하나를 시작하기 위해서 많은 자원이 필요로 한다.
- JVM을 실행하는데 최소 32~64MB의 물리 메모리를 점유한다.
- 쓰레드 하나를 추가하는데 1MB 이내의 메모리를 점유한다.
- 그렇기에 쓰레드를 "경량 프로세스"라고 부른다.
- 또한 단일 쓰레드보다 다중 쓰레드로 실행하는 것이 더 빠른 결과를 제공한다.
- 프로세스 하나를 시작하기 위해서 많은 자원이 필요로 한다.
- 쓰레드를 생성하는 방법
- Runnable 인터페이스를 사용하는 방법
- Thread 클래스를 사용하는 방법
- Thread 클래스는 Runnable 인터페이스를 구현한 클래스.
- 두 방법 모두 java.lang 패키지에 존재하므로 별도의 import가 필요 없다.
Runnable 인터페이스 / Thread 클래스
- Runnable 인터페이스의 메소드
- void run() : 쓰레드가 시작되면 수행되는 메소드
- Thread 클래스의 메소드는 많은 생성자와 메소드를 제공한다.
- Runnable 인터페이스를 구현한 클래스 / Thread 클래스를 확장한 클래스 비교
- 두 클래스 모두 쓰레드를 실행할 수 있다는 공통점이 존재한다.
- 쓰레드 클래스를 실행하는 방식이 서로 다르다.
- 쓰레드 클래스를 구현하거나 확장할 때 run() 메소드를 시작점으로 작성해야만 한다.
- main() 메소드가 자바 프로그램의 시작점인 것처럼 쓰레드의 시작점은 run()
- 쓰레드를 시작하는 메소드는 run()이 아닌 start()
- start() 메소드를 따로 만들어놓지 않아도 자바에서 자동으로 run() 메소드를 수행하도록 되어있다.
- Runnable 인터페이스를 구현한 클래스는 쓰레드로 바로 시작할 수 없다.
- Thread 클래스의 생성자에 해당 객체를 추가하여 start()로 시작해야한다.
- new Thread(객체명).start();
- Thread 클래스를 확장한 클래스는 바로 쓰레드를 실행할 수 있다.
- 객체명.start();
- 그렇다면 왜 쓰레드를 생성하는 방법을 두 가지나 제공할까?
- 한 가지 방식은 인터페이스를 구현하는 방식이고 나머지는 클래스를 확장하는 방식이다.
- 만약 인터페이스 방식이 존재하지 않는다면 쓰레드로 구현도 해야하고 상속받아야 하는 부모 클래스도 존재하는 경우 자바에서는 다중 상속을 할 수 없기 때문에 문제가 발생한다.
- 그렇기에 쓰레드 클래스가 다른 클래스를 확장할 필요가 있는 경우 Runnable 인터페이스를 구현하면 되고 그렇지 않은 경우에는 Thread 클래스를 확장해서 사용하는 것이 편하다.
- 한 가지 방식은 인터페이스를 구현하는 방식이고 나머지는 클래스를 확장하는 방식이다.
- 쓰레드를 start() 메소드를 통해서 시작했다는 것은 프로세스가 아닌 하나의 쓰레드를 JVM에 추가하여 실행한다는 것
- 쓰레드를 구현할 때 start() 메소드를 호출하면 쓰레드 클래스에 있는 run() 메소드의 내용이 끝나든 끝나지 않든 쓰레드를 시작한 메소드에서는 그 다음 줄에 있는 코드를 실행한다.
- 여러 쓰레드가 실행되었다면 쓰레드 종료 순서는 실행 순서와 상관 없이 누가 먼저 종료될지 모른다.
- 쓰레드는 run() 메소드 종료시 끝난다.
- run() 메소드가 끝나지 않으면 실행한 애플리케이션은 끝나지 않는다.
- 데몬 쓰레드는 예외....
- Thread 생성자 매개 변수로 Runnable 객체, 쓰레드 이름, 쓰레드 그룹, stackSize가 들어갈 수 있다.
- 모든 쓰레드는 이름이 있다.
- 별도의 이름을 지정할 수 있다.
- Thread 클래스를 구현한 클래스에서 생성자를 생성해 super()로 생성자를 지정해 생성한다.
- 별도로 지정하지 않으면 "Thread-n"이 된다.
- n은 쓰레드가 생성된 순서에 따라 증가한다.
- 쓰레드 이름이 겹친다고 해서 예외나 에러가 발생하지 않는다.
- 별도의 이름을 지정할 수 있다.
- ThreadGroup : 쓰레드를 생성 시 쓰레드끼리 묶어놓을 수 있는 것
- 쓰레드 그룹으로 묶으면 ThreadGroup 클래스에서 제공하는 여러 메소드를 통해 각종 정보를 얻을 수 있다.
- stackSize : 스택의 크기를 의미하며 자바 프로세스 시작 시 실행 데이터 공간(Runtime data aread)가 구성되며 그 중 Stack이라는 공간이 있고 쓰레드가 생성될 때마다 별도의 스택이 할당된다.
- Thread 클래스에는 static 메소드가 많이 있다.
- Thread에 있는 static 메소드는 대부분 해당 쓰레드를 위해서 존재하는 것이 아닌 JVM에 있는 쓰레드를 관리하기 위한 용도의 메소드다.
- 그 중 예외는 sleep() 메소드
- Thread에 있는 static 메소드는 대부분 해당 쓰레드를 위해서 존재하는 것이 아닌 JVM에 있는 쓰레드를 관리하기 위한 용도의 메소드다.
- Thread 클래스의 static 메소드 sleep()
- static void sleep(long millis) : 매개 변수로 넘어온 시간(1/1000초)만큼 대기한다.
- Thread.sleep() 메소드를 사용할 때 항상 try-catch로 묶어줘야 한다.
- sleep() 메소드는 InterruptedException을 던질수 있다고 선언되어 있기 때문
- sleep() 메소드로 대기하고 있는 중에 interrupt가 될 수 있으므로, InterruptedException 이 발생할 수 있다.
- InterruptedException이나 상위 예외 (Exception 등...)으로 catch 해줘야 한다.
- sleep() 메소드는 InterruptedException을 던질수 있다고 선언되어 있기 때문
- Thread 클래스의 메소드는 쓰레드 속성을 확인 / 지정하기 위한 메소드와 상태를 통제하기 위한 메소드로 나뉜다.
- void run() : 쓰레드가 시작되면 수행되는 메소드
- long getId() : 쓰레드의 고유 ID를 리턴한다. (JVM에서 자동으로 생성)
- String getName() : 쓰레드의 이름을 리턴한다.
- void setName(String name) : 쓰레드의 이름을 지정한다.
- int getPriority() : 쓰레드의 우선 순위를 확인한다.
- void setPriority(int newPriority) : 쓰레드의 우선 순위를 지정한다.
- boolean isDaemon() : 쓰레드가 데몬인지 확인한다.
- void setDaemon(boolean on) : 쓰레드를 데몬으로 설정할지 아닌지를 설정한다.
- StackTraceElement[] getStackTrace() : 쓰레드의 스택 정보를 확인한다.
- Thread.State getState() : 쓰레드의 상태를 확인한다.
- ThreadGroup getThreadGroup() : 쓰레드의 그룹을 확인한다.
- 쓰레드의 우선 순위 (Priority): 대기하고 있는 상황에서 더 먼저 수행할 수 있는 순위를 의미한다.
- 우선 순위 값은 기본 값으로 사용하는 것을 권장한다.
- 되도록 우선 순위를 지정하는 것을 피해라
- 장애가 발생할 수 있다.
- 우선 순위를 지정해야 하는 경우 사용하는 상수
- MAX_PRIORITY : 가장 높은 우선 순위로 값은 10이다.
- NORM_PRIORITY : 일반 쓰레드의 우선 순위로 값은 5다.
- MIN_PRIORITY : 가장 낮은 우선 순위로 값은 1이다.
- 우선 순위 값은 기본 값으로 사용하는 것을 권장한다.
- 데몬 쓰레드(Daemon Thread)란?
- 일반 쓰레드는 쓰레드가 종료되기 전까지 JVM이 종료될 수 없다.
- 데몬 쓰레드의 경우 쓰레드가 수행되고 있든, 수행되지 않든 상관 없이 JVM이 종료될 수 있다.
- 데몬 쓰레드 외에 실행중인 다른 일반 쓰레드가 없다면 데몬 쓰레드를 종료한다.
- 데몬 쓰레드로 지정하기 위해서 해당 쓰레드가 시작(start() 메소드 호출)전에 데몬 쓰레드로 지정되어야 한다.
- 쓰레드 객체 명.setDaemon(true); 로 해당 쓰레드를 데몬 쓰레드로 지정한다.
- 쓰레드가 시작한 다음 데몬 쓰레드로 지정할 수 없다.
- 데몬 쓰레드는 주로 부가적인 작업을 수행하는 쓰레드를 선언할 때 데몬 쓰레드로 만든다.
- ex) 모니터링 기능의 쓰레드
Synchronized
- 쓰레드와 관련이 있는 자바의 예약어 중 하나
- 어떤 클래스나 메소드가 쓰레드에 안전(Thread Safe)하려면 synchronized를 사용해야만 한다.
- 여러 쓰레드가 동일한 한 객체에 선언된 메소드에 접근하여 데이터를 처리하려고 하면 동시에 연산을 수행해 값이 꼬이는 경우가 발생할 수 있다.
- 메소드에서 인스턴스 변수를 수정하려고 할 때에만 이런 문제가 발생한다.
- 매개 변수나 지역 변수만 다루는 메소드는 괜찮다.
- synchronized 사용 방법
- synchronized methods : 메소드 자체를 synchronized로 선언하는 방법
- 메소드 선언문에 리턴 타입 앞에 synchronized를 명시해주면 된다.
- synchronized methods 사용 시 동일한 객체의 이 메소드에 접근 시 몇 개의 쓰레드가 접근을 하더라도 하나의 쓰레드만 이 메소드를 수행하게 된다.
- 동일한 객체를 참조하는 다른 쓰레드에서 이 메소드를 변경하려고 하면 먼저 들어온 쓰레드가 종료될 때까지 기다린다.
- 이 경우 성능상 문제점이 발생할 수 있다.
- 메소드 자체가 synchronized로 인해 한 쓰레드가 종료될 때까지 기다릴 필요가 없는 문장들까지 대기 시간이 발생하게 된다.
- synchronized statements : 메소드 내의 특정 문장만 synchronized로 감싸는 방법
- synchronized(객체) { } 로 표현한다.
- 중괄호 내에 있는 연산만 동시에 여러 쓰레드에서 처리할 수 없도록 한다.
- 소괄호 안의 객체는 잠금 처리를 하기 위한 객체로 별도의 객체를 선언해 사용한다.
- 여기서 객체는 문지기의 역할과 같다.
- 문지기는 한 쓰레드만 일을 할 수 있도록 허락해주고 그 쓰레드가 일을 다 처리하고 나오면 대기하고 있는 다음 쓰레드에게 기회를 준다.
- 문지기 역할의 객체를 두 개 이상 만들어 사용할 수 있다.
- ex) a, b 두 개의 변수를 처리할 때 synchronized 블록을 사용하는 경우
lock이라는 하나의 객체만을 사용한다면 a라는 변수를 처리할 때 b라는 변수를 처리하는 부분도 처리하지 못하게 된다.
그렇기에 lockA, lockB와 같이 별도의 객체로 선언을 해 각각의 블록에 다른 객체를 사용하는 것이 효율적이다.
- ex) a, b 두 개의 변수를 처리할 때 synchronized 블록을 사용하는 경우
- synchronized(객체) { } 로 표현한다.
- synchronized methods : 메소드 자체를 synchronized로 선언하는 방법
// Synchronized Methods Example
public int amount;
public synchronized void plus (int value){
amount += value;
}
// Synchronized Statements Example
Object lock = new Object(); //잠금 처리를 하기 위한 별도의 객체를 선언한다.
public void plus2(int value){
synchronized(lock) { //필요한 부분만 synchronized로 감싼다.
amount += value;
}
}
- synchronized 사용 시 주의할 점
- 동일한 객체가 아닌 서로 다른 객체를 참조한다면 synchronized로 선언된 메소드는 synchronized를 안쓰는 것과 같기에 사용할 이유가 없다.
- 인스턴스 변수가 선언되어 있더라도 다른 쓰레드에서 공유할 일이 없다면 synchronized를 사용할 이유가 없다.
- StringBuffer / StringBuilder
- StringBuffer는 synchronized 블록으로 주요 데이터를 감싸두었기 때문에 Thread Safe하며 하나의 문자열 객체를 여러 쓰레드에서 공유하는 경우 사용한다.
- StringBuilder는 synchronized가 사용되지 않아 Thread Unsafe하며 여러 쓰레드에서 공유할 일이 없을 때 사용한다.
쓰레드(Thread)를 통제하는 메소드
- 쓰레드를 통제하는 메소드들
- Thread.State getState() : 쓰레드의 상태를 확인한다.
- void join() : 수행중인 쓰레드가 중지할 때까지 무한대로 대기한다.
- 매개 변수로 밀리초(1/1000)와 나노초(1/1000000000)를 명시할 경우 지정한 시간동안 대기할 수 있다.
- 밀리초를 음수로 하거나 나노초를 0 ~ 999,999 사이의 값을 하지 않으면 IllegalArgumentException 예외 발생
- 매개 변수로 밀리초(1/1000)와 나노초(1/1000000000)를 명시할 경우 지정한 시간동안 대기할 수 있다.
- void interrupt() : 수행중인 쓰레드에 중지 요청을 한다.
- InterruptedException 예외를 발생시키며 쓰레드를 중지시킨다.
- sleep()과 join(), wait() 같이 대기 상태를 만드는 메소드가 호출됐을 때만 interrupt() 메소드를 호출할 수 있다.
- 호출하면 예외가 발생하지만 대기 상태에서 풀려날 수 있다.
- 쓰레드 시작 전이나 종료된 상태에서 interrupt() 호출 시 예외 / 에러 없이 넘어간다.
- void checkAccess() : 현재 수행중인 쓰레드가 해당 쓰레드를 수정할 수 있는 권한이 있는지 확인한다.
- 권한이 없는 경우 SecurityException 예외를 발생한다.
- boolean isAlive() : 쓰레드가 살아있는지 확인한다.
- run() 메소드가 종료되었는지 아닌지를 확인하는 것
- boolean isInterrupted() : run() 메소드가 정상적으로 종료되지 않고 interrupt() 메소드의 호출을 통해 종료되었는지를 확인하는데 사용한다.
- 다른 쓰레드에서 확인할 때 사용한다.
- static boolean interrupted() : 현재 쓰레드가 중지되었는지 확인한다.
- 본인의 쓰레드를 확인할 때 사용한다.
- Thread.State란?
- Thread 클래스에서 public static State라는 enum 클래스로 상수들을 가지고 있다.
- NEW : 쓰레드 객체는 생성됐지만 아직 시작되지 않은 상태
- RUNNABLE : 쓰레드가 실행중인 상태
- BLOCKED : 쓰레드가 실행 중지 상태이며, 모니터 락(monitor lock)이 풀리기를 기다리는 상태
- WAITING : 쓰레드가 대기중인 상태
- TIMED_WAITING : 특정 시간만큼 쓰레드가 대기중인 상태
- TERMINATED : 쓰레드가 종료된 상태
- Thread 클래스에서 public static State라는 enum 클래스로 상수들을 가지고 있다.
- 모든 쓰레드는 "NEW - 상태 - TERMINATED"의 라이프 사이클을 가진다.
- 여기서 상태는 NEW / TERMINATED를 제외한 모든 상태를 의미한다.
- Thread 클래스의 stop() 메소드는 안전상의 이유로 deprecated 되었다.
- 그렇기에 interrupt()를 호출해 쓰레드를 멈춰야한다.
- Thread 클래스의 static 메소드
- static int activeCount() : 현재 스레드가 속한 쓰레드 그룹의 쓰레드 중 살아있는 쓰레드의 개수를 리턴한다.
- static Thread currentThread() : 현재 수행중인 쓰레드의 객체를 리턴한다.
- static void dumpStack() : 콘솔 창에 현재 쓰레드의 스택 정보를 출력한다.
쓰레드와 관련된 Object 클래스의 메소드
- 쓰레드와 관련된 Object 클래스의 메소드
- void wait() : 다른 쓰레드가 Object 객체에 대한 notify() / notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다.
- wait() 메소드 호출 시 쓰레드의 상태는 WAITING이 된다.
- 매개 변수로 시간을 지정함으로 지정한 시간만큼 대기하도록 할 수 있다.
- void notify() : Object 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다.
- 대기하고 있는 쓰레드가 여러 개인 경우 먼저 대기하고 있는 것부터 WAITING 상태를 풀어준다.
- 여러 개의 WAITING 상태를 풀어주려면 쓰레드 개수 만큼 notify()를 호출하던 notifyAll()을 호출해야 한다.
- void notifyAll() : Object 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다.
- void wait() : 다른 쓰레드가 Object 객체에 대한 notify() / notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다.
ThreadGroup에서 제공하는 메소드
- ThreadGroup은 쓰레드 관리를 용이하게 하기 위한 클래스
- tree 구조를 가진다.
- 하나의 그룹이 다른 그룹에 속할 수 있고 아래에 또 다른 그룹을 포함할 수 있다.
- ThreadGroup 클래스의 주요 메소드
- int activeCount() : 실행중인 쓰레드의 개수를 리턴한다.
- int activeGroupCount() : 실행중인 쓰레드 그룹의 개수를 리턴한다.
- int enumerate(Thread[] list) : 현재 쓰레드 그룹에 있는 모든 쓰레드를 매개 변수 쓰레드 배열에 담는다.
- 매개 변수로 boolean도 명시하는 경우 true라면 하위에 있는 쓰레드 그룹에 있는 쓰레드 목록도 포함한다.
- 매개 변수로 ThreadGroup을 명시하는 경우 쓰레드 그룹을 매개 변수 쓰레드 그룹에 담는다.
- 리턴 값은 배열에 저장된 쓰레드의 개수.
- String getName() : 쓰레드 그룹의 이름을 리턴한다.
- ThreadGroup getParent() : 부모 쓰레드 그룹을 리턴한다.
- void list() : 쓰레드 그룹의 상세 정보를 출력한다.
- void setDaemon(boolean daemon) : 지금 쓰레드 그룹에 속한 쓰레드들을 데몬으로 지정한다.
간단 내용 정리
1. 쓰레드와 프로세스의 차이를 이야기 해 보세요.
프로세스와 쓰레드의 가장 큰 차이는 사용하는 자원의 차이다. 프로세스 하나가 실행하기 위해서는 많은 메모리를 필요로 하지만, 쓰레드는 상대적으로 적은 메모리를 필요로한다.
일반적인 프로그램은 프로세스 하나에 하나 이상의 쓰레드가 수행된다.
2. 쓰레드 클래스를 만들기 위해서는 어떤 인터페이스를 구현하면 될까요?
java.lang.Runnable 인터페이스
3. 위의 문제에서 이야기한 인터페이스에 선언되어 있는 유일한 메소드는 무엇인가요?
public void run()
4. 쓰레드 클래스를 만들기 위해서 어떤 클래스를 확장하면 되나요?
java.lang.Thread 클래스
5. 쓰레드가 시작되는 메소드의 이름은 무엇인가요?
run()
6. 쓰레드를 시작하는 메소드의 이름은 무엇인가요?
start()
7. 쓰레드에 선언되어 있는 sleep() 메소드의 역할은 무엇인가요?
매개 변수로 지정된 시간만큼 쓰레드를 멈춘다.
8. sleep() 메소드를 사용할 때 try-catch 로 감싸 주어야 하는 이유는 무엇인가요?
sleep() 메소드로 대기하고 있는 중에 interrupt가 될 수 있으므로, InterruptedException 이 발생할 수 있다.
9. 데몬(Daemon) 쓰레드와 일반 쓰레드의 차이는 무엇인가요?
일반 쓰레드는 쓰레드 종료 전까지 프로세스가 종료되지 않지만 데몬 쓰레드는 실행 여부와 상관 없이 다른 실행중인 쓰레드가 없으면 프로세스가 종료된다
10. synchronized 구문은 왜 써 주며, 어디에 사용해야 하나요?
synchronized 구문은 동시에 여러 쓰레드에서 하나의 값에 접근하려고 할 때 데이터의 정합성을 지키기 위해서 사용한다.
그러므로, 여러 쓰레드에서 동시에 접근할 일이 있을 경우에만 써야한다.
그렇지 않으면 성능상 큰 문제가 발생할 수 있다.
11. synchronized 를 사용하는 두 가지 방법은 어떤 것 인가요?
메소드 자체를 synchronized로 선언하는 방법
메소드 내 필요한 부분만 synchronized 블록으로 감싸는 방법
12. 쓰레드의 상태에는 어떤 것들이 있나요?
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
13. 쓰레드에 선언되어 있는 join() 메소드의 용도는 무엇인가요?
해당 쓰레드가 종료할 때까지 기다린다.
14. 쓰레드에 선언되어 있는 interrupt() 메소드의 용도는 무엇인가요?
해당 쓰레드에게 종료하도록 요청한다.
15. interrupt() 메소드를 호출하면 해당 쓰레드는 어떤 상태에 있을 때 interrupt() 메소드가 호출된 효과가 발생 되나요?
sleep(), join(), wait() 메소드가 호출되어 대기중인 상태인 쓰레드에게만 interrupt() 메소드가 사용 가능하다.
16. Object 클래스에 선언된 wait() 메소드의 용도는 무엇인가요?
다른 쓰레드가 notify()나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드를 대기시킨다.
매개 변수가 존재하는 경우 지정된 시간만큼 대기시킨다.
17. Object 클래스에 선언된 notify() 메소드의 용도는 무엇인가요?
wait()으로 대기하고 있는 단일 쓰레드를 깨우는 용도
18. ThreadGroup 클래스에 선언된 enumerate() 메소드의 용도는 무엇인가요?
쓰레드 그룹의 모든 쓰레드를 매개 변수 쓰레드 배열에 담는다.