✔️이 글은 [자바의 신 - 이상민 지음] 도서를 바탕으로 정리한 글입니다.
쓰레드가 도대체 뭘까?
JVM이 시작되면 자바 프로세스가 시작하는데 이 프로세스 안에서 여러개의 쓰레드라는 것이 수행될 수 있다.
java 명령어를 사용하여 클래스를 실행시키는 순간 자바 프로세스가 시작되고, main() 메소드가 수행되면서 하나의 쓰레드가 시작되는 것이다. 만약 많은 쓰레드가 필요하다면 main() 메소드에서 쓰레드를 생성해주면 된다.
자바로 웹을 제공할 때 WAS를 사용하는데 이 WAS도 main 메소드에서 생성한 쓰레드들이 수행되는 것이다.
프로세스가 하나 시작하려면 많은 자원이 필요한데 하나의 작업을 동시에 수행하려고 여러 프로세스를 띄워서 수행하면 각각 메모리를 할당해줘야 해서 성능적으로 좋지 않다. 반면에 쓰레드 하나 만드는 건 프로세스에 비해 작은 공간의 메모리를 점유하기 때문에 여러 쓰레드를 사용하는 것이 공간도 덜 차지하고 빠른 처리를 하는데 도움을 준다. 쓰레드가 작은 공간만 차지해서 "경량 프로세스(lightweight process)"라고도 부른다.
Runnable 인터페이스와 Thread 클래스
쓰레드를 생성하는 방법 2가지
- Runnable 인터페이스 사용
- Thread 클래스 사용
Runnable 인터페이스와 Thread 클래스는 java.lang 패키지에 있다.
Runnable 인터페이스에 선언된 메소드는 run()이라는 메소드 하나로 쓰레드가 시작되면 수행되는 메소드이다.
Runnable 인터페이스로 구현한 클래스로 만든 쓰레드 객체로는 바로 시작할 수 없고 new Thread(쓰레드객체).start()를 해줘야 시작된다.
반면에 Thread는 Runnable 인터페이스를 구현한 클래스로 Runnable보다 더 많은 메소드를 제공한다.
또 Thread로 만든 쓰레드 객체는 바로 .start()로 실행할 수 있다.
쓰레드를 시작하는 메소드는 start()이고, start() 메소드에서 run() 메소드를 실행시킨다. 우리가 쓰레드에게 할당할 일은 run() 메소드에서 일어난다. 사용자가 start() 메소드를 따로 만들어주지 않아도 자바에서 알아서 run() 메소드를 실행시켜준다.
Thread와 Runnable 두가지 방법이 있는 이유는 자바에선 extends로 상속이 하나만 가능해서 어떤 부모 클래스를 가지면서 Thread 클래스를 확장할 수 없기 때문에 Runnable이라는 인터페이스를 구현함으로써 어떤 클래스의 상속을 받는 쓰레드 클래스를 만들어 줄 수 있다.
start() 메소드로 쓰레드를 시작시키면 원래 main문이 있는 쓰레드에서 start() 메소드로 시작된 쓰레드를 기다려주지 않고 바로 다음 줄의 코드를 수행한다. 따라서 생겨난 쓰레드의 코드가 먼저 수행될 수도 있고 start() 메소드 다음에 오는 코드가 먼저 수행될 수도 있다. 수행 순서는 OS 스케쥴러가 정해줘서 사용자가 수행되는 순서를 알기는 어렵다.
새로 생성된 쓰레드는 run() 메소드가 종료되면 끝난다.
Thread 클래스의 생성자를 살펴보자.
쓰레드에는 "Thread-n"이라는 이름을 갖는데 n에는 쓰레드가 생성된 순서에 따라 0부터 숫자를 매겨준다. 쓰레드 이름을 따로 지정해줄 수도 있다.
쓰레드를 생성할 때 여러 쓰레드들을 ThreadGroup으로 묶어놓을 수 있다. 그룹을 묶으면 ThreadGroup 클래스에서 제공하는 여러 메소드를 통해 각종 정보를 얻을 수 있다.
자바의 실행 데이터 공간 중 Stack이라는 공간은 쓰레드가 생성될 때마다 별도의 Stack이 할당되는데, 이 Stack의 크기를 지정해줄 수 있따. 경우에 따라서는 stackSize를 지정해줘도 무시될 수도 있다.
Thread 생성자들
- Thread()
- Thread(Runnable target)
- Thread(Runnable target, String name)
- Thread(String name)
- Thread(ThreadGroup group, Runnable target) :
- Thread(ThreadGroup group, Runnable target, String name) :
- Thread(ThreadGroup group, Runnable target, String name, long stackSize) :
- Thread(ThreadGroup group, String name)
많이 사용되는 sleep() 메소드에 대해서 살펴보자
Thread 클래스에는 static 메소드와 deprecated 된 메소드가 많이 있다. static 메소드는 대부분 JVM에 있는 쓰레드를 관리하기 위한 용도로 사용되는데 해당 쓰레드를 위해 사용되는 메소드에는 sleep()이 있다.
sleep() 메소드에는 최소 하나에서 최대 두개의 매개변수를 넘겨줄 수 있는데 첫번째 매개변수(1/1000초)만큼 대기하고, 두번째 매개변수(1/1000000000초)가 있으면 첫번째 매개변수에 더한 시간만큼 대기한다. 두번째 매개변수는 첫번째 매개변수의 단위 밀리세컨드보다 작아야 한다. 밀리세컨드보다 큰 값을 넣어주면 IllegalArgumentException이라는 예외가 발생한다.
sleep() 메소드는 인터럽트를 받았을 때 현재 작업을 취소하고 즉시 반환하는 InterruptedException 예외가 발생할 수도 있어 sleep() 메소드를 사용할 때에는 항상 try-catch문으로 묶어주고 적어도 InterruptedException으로 catch해줘야 한다.
main() 메소드의 수행이 끝나더라도 다른 메소드에서 시작한 쓰레드가 종료하지 않으면 해당 자바 프로세스는 끝나지 않는다.
Thread 클래스의 주요 메소드를 살펴보자
Thread의 속성을 확인하고 지정하는 메소드
- run()
- getId() : 쓰레드의 고유 id 리턴, id는 JVM에서 자동으로 생성해줌
- getName() : 쓰레드 이름 리턴
- setName(String name) : 쓰레드 이름 설정
- getPriority() : 우선순위 리턴
- setPriority(int newPriority) : 우선순위 지정
- isDaemon() : 데몬인지 확인
- setDaemon(boolean on) : 매개변수가 true면 쓰레드를 데몬으로 만들어줌
- getStackTrace() : 스택 정보 확인
- getState() : 상태 확인
- getThreadGroup() : 쓰레드 그룹 확인
쓰레드의 우선순위(Priority)는 대기하고 있는 상황에서 더 먼저 수행할 수 있는 순위로 기본값을 사용하는 것을 권장한다. 만약 설정해주고 싶다면 우선순위와 관련된 상수 MAX_PRIORITY(10), NORM_PRIORITY(5), MIN_PRIORITY(1)를 사용하여 정하는것을 권장한다.
데몬 쓰레드로 지정된 쓰레드는 수행되고 있든, 수행되지 않고 있든 상관없이 JVM이 끝날 수 있다.
즉 더이상 돌아가는 일반 쓰레드가 없다면 데몬 쓰레드도 종료된다.
데몬 쓰레드 지정은 start() 메소드 호출 전에 해줘야만 한다.
이런 데몬 쓰레드는 모니터링하는 쓰레드처럼 부가적인 작업을 수행하는 쓰레드로 주로 사용된다.
쓰레드와 관련이 많은 synchronized
여러 쓰레드가 한 객체에 선언된 메소드에 접근해 인스턴스 변수를 수정하려고 할 때 동시에 연산을 수행하여 값이 꼬이는 경우가 발생할 수도 있다. 이런 경우를 Thread safe하지 못한다고 한다.
어떤 클래스나 메소드가 쓰레드에 안전하려면, synchronized 키워드를 사용해야 한다.
메소드를 synchronized로 선언하려면 메소드 선언부에 리턴 타입 앞 어디든 synchronized 예약어를 넣어주면 된다.
동일한 객체의 synchornized를 써준 메소드에 여러개의 쓰레드가 접근해도 한 순간에는 하나의 쓰레드만 이 메소드를 수행하게 된다.
synchronized 블록은 이렇게 사용한다
메소드 전체를 synchronized로 선언한다면, 한 쓰레드만 접근할 수 있게 해줄 변수를 다루는 코드 외의 코드들이 대기해야 하는 단점이 생긴다. 이러한 경우엔 그 변수를 처리하는 부분만 synchronized 블록으로 묶어주면 된다.
synchronized(this) {
//변수 처리 코드
}
this가 들어있는 부분에는 잠금처리를 하기 위한 객체를 선언하는데 일반적으론 별도의 객체를 Object lock = new Object()로 선언해서 넣어준다. this나 lock 객체는 한 명의 쓰레드만 일을 할 수 있도록 허용해주고, 쓰레드가 일을 다 처리하고 나오면 대기하고 있던 쓰레드에게 기회를 주는 역할을 한다.
만약 여러개의 변수에 synchronized 블록 처리를 해주고 싶을 땐 각 변수별로 별도의 lock 객체를 사용하면 된다.
두 개의 쓰레드가 서로 다른 객체를 참조한다면 synchronzied로 선언된 메소드는 같은 객체를 참조하는 것이 아니라 의미가 없어진다.
예시로 StringBuffer는 쓰레드에 안전하고, StringBuilder는 쓰레드에 안전하지 않은 이유도 StringBuffer는 synchronized 블록으로 주요 데이터 처리 부분을 감싸두었기 떄문이다. 따라서 StringBuffer는 여러 쓰레드에서 주요한 데이터를 처리해야 하는 경우에 사용한다.
쓰레드를 통제하는 메소드들
쓰레드의 상태를 통제하는 메소드
- getState() : 쓰레드의 상태를 확인(리턴 타입 : Thread.State)
- join() : 수행 중인 쓰레드가 중지할 때까지 대기, 매개변수로 최대 2개가능(첫번째-밀리세컨드, 두번째-나노세컨드)
- interrupt() : 수행중인 쓰레드에 중지 요청
Thread클래스에는 State라는 public static으로 선언된 enum 클래스가 있다.
- NEW : 쓰레드 객체는 생성되었지만, 시작되지는 않은 상태
- RUNNABLE : 쓰레드가 실행 중인 상태
- BLOCKED : 쓰레드가 실행 중지 상태, 모니터락(monitor lock)이 풀리기를 기다리는 상태
- WAITING : 쓰레드가 대기 중인 상태
- TIMED_WAITING : 특정 시간만큼 쓰레드가 대기 중인 상태
- TERMINATED : 쓰레드가 종료된 상태
join() 메소드는 해당 쓰레드가 종료될 때까지 기다리는데 변수가 없는 join() 메소드는 쓰레드가 끝날때까지 무한대로 대기한다.
매개변수를 지정해주면 대기시간으로 첫번째는 밀리초, 두번째는 나노초를 지정가능하다. 밀리초만 써줘도 된다. 다만 나노초는 1밀리세컨드보다 작은 값이 나와야 한다. 그렇지 않으면 sleep()처럼 IllegalArgumentException 예외가 발생한다.
매개변수로 0을 넘겨주면 매개변수가 없는 join()과 동일하게 동작한다.
interrupt() 메소드는 현재 수행중인 쓰레드를 중단시키고, InterruptedException이라는 예외를 발생시킨다. sleep()과 join() 메소드와 같이 대기 상태를 만드는 메소드가 호출되었을 때는 interrupt() 메소드를 호출할 수 있다. 이외에도 Object 클래스의 wait() 메소드가 호출된 상태에서도 interrupt() 메소드 호출이 가능하다.
쓰레드 시작 전이나 종료된 상태에서 interrupt()를 호출하면 아무 일도 없이 그냥 다음 문장으로 넘어간다.
새로 생성한 쓰레드는 2초를 기다리는데 main문에서의 join()메소드의 매개변수로 1000을 넘겨주면 1초만 기다려주라는 의미로 1초 뒤엔 다음 코드가 실행된다.
상태 확인을 위한 메소드
- checkAccess() : 현재 수행 중인 쓰레드가 해당 쓰레드를 수정할 수 있는 권한이 있는지 확인. 없다면 SecurityException 예외 발생
- isAlive() : 쓰레드가 살아있는지 확인(run() 메소드 종료여부 확인)
- isInterrupted() : 다른 쓰레드의 run()이 interrupt() 메소드로 종료되었는지 확인
- interrupted() : static 메소드로 현재 쓰레드가 중지되었는지 확인
주요 static 메소드
- activeCount() : 현재 쓰레드가 속한 쓰레드 그룹에서 살아있는 쓰레드 개수리턴
- currentThread() : 현재 수행 중인 쓰레드 객체 리턴
- dumpStack() : 콘솔 창에 현재 쓰레드의 스택 정보 출력
Object 클래스에 선언된 쓰레드와 관련있는 메소드들
Object 클래스에는 쓰레드의 상태를 통제하는 메소드들이 선언되어 있다.
wait() 메소드를 쓰면 쓰레드가 대기 상태(WAITING)가 되고, 다른 쓰레드가 Object 객체에 대한 nofity()나 notifyAll() 메소드로 쓰레드 대기 상태를 해제할 수 있다.
wait() 메소드의 매개변수는 join처럼 대기 시간 지정이 가능하다.
자바에서 nofity() 메소드를 호출하면 먼저 대기하고 있는 것부터 WAITING 상태를 해제해준다.
ThreadGroup에서 제공하는 메소드들
하나의 애플리케이션에는 여러 종류의 쓰레드가 있을 수 있어 ThreadGroup 클래스가 쓰레드 관리를 용이하게 해준다.
쓰레드 그룹은 기본적으로 트리 구조를 가진다.
ThreadGroup의 주요 메소드
- activeCount() : 실행 중인 쓰레드 개수 리턴
- activeGroupCount() : 실행 중인 쓰레드 그룹 개수 리턴
- enumerate(Thread[] list) : 현재 쓰레드 그룹에 있는 모든 쓰레드를 매개변수로 넘어온 배열에 담음
- enumerate(Thread[] list, boolean recurse) : 현재 쓰레드 그룹에 있는 모든 쓰레드를 매개변수로 넘어온 배열에 담고, recurse가 true면 하위에 있는 쓰레드 그룹에 있는 쓰레드 목록도 포함
- enumerate(ThreadGroup[] list) : 쓰레드 그룹에 있는 모든 쓰레드 그룹을 매개변수로 넘어온 배열에 담음
- enumerate(ThreadGroup[] list, boolean recurse)
- getName() : 쓰레드 그룹 이름 리턴
- getParent() : 부모 쓰레드 그룹을 리턴
- list() : 쓰레드 그룹의 상세 정보 출력
- setDaemon(boolean daemon) : 매개 변수가 true면 지금 쓰레드 그룹에 속한 쓰레드들을 데몬으로 지정
enumerate()의 리턴 값은 배열에 저장된 쓰레드의 개수이다. 쓰레드 그룹에 있는 모든 쓰레드 객체를 제대로 담으려면 activeCount()로 개수를 구해 그 개수만큼 배열을 생성하면 된다.
정리해 봅시다
1. 쓰레드와 프로세스의 차이?
프로세스는 하나가 실행되기 위해서 많은 메모리를 필요로 하지만, 쓰레드는 상대적으로 적은 메모리를 필요로 한다.
일반적인 프로그램은 프로세스 하나에 하나 이상의 쓰레드가 수행된다.
2. 여러분들이 쓰레드 클래스를 만들기 위해서는 어떤 인터페이스를 구현하면 될까요?
Runnable
3. 2에서 이야기한 인터페이스에 선언되어 있는 유일한 메소드는?
run()
4. 쓰레드 클래스를 만들기 위해서는 어떤 클래스를 확장하면 되나요?
Thread
5. 쓰레드가 시작되는 메소드의 이름은?
run()
6. 쓰레드를 시작하는 메소드의 이름은?
start()
7. 쓰레드에 선언되어 있는 sleep() 메소드의 역할은?
매개변수로 넘겨준 시간만큼 해당 쓰레드를 멈춘다.
8. sleep() 메소드를 사용할 때에는 try-catch로 감싸주어 예외를 처리해 주어야 하는데, 그 이유는?
sleep() 메소드로 대기하고 있는 중에 interrupt가 발생할 수 있으므로 InterruptedException 예외처리를 해줘야 한다.
9. 데몬(Daemon) 쓰레드와 일반 쓰레드의 차이는?
데몬 쓰레드는 프로세스가 종료되는 상황이 되었을 때 해당 쓰레드가 종료되지 않아도 실행중인 일반 쓰레드가 없으면 해당 프로세스는 중지된다.
10. synchronized 구문은 왜 써주며, 어디에 사용해야 하나요?
동시에 여러 쓰레드에서 하나의 값에 접근하려고 할 때 데이터의 정합성을 지키기 위해 사용한다.
11. synchronized를 사용하는 두 가지 방법은 어떤 것인가요?
메소드 자체를 synchronized로 선언하거나 메소드 내에 필요한 부분만 synchronized 블록으로 묶어주는 방법이 있다.
12. 쓰레드의 상태에는 어떤 것이 있나요?
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED로 나뉜다.
13. 쓰레드에 선언되어 있는 join() 메소드의 용도는?
해당 쓰레드가 종료될 때까지 대기한다.
14. 쓰레드에 선언되어 있는 interrupt() 메소드의 용도는?
해당 쓰레드를 종료시킨다.
15. interrupt() 메소드를 호출하면 해당 쓰레드는 어떤 상태에 있을 때 interrupt() 메소드가 호출된 효과가 발생되나요?
wait(), sleep(), join()이 호출되어 대기중인 상태에만 interrupt(0 메소드가 호출된 효과가 발생한다.
16. Object 클래스에 선언된 wait() 메소드의 용도는?
다른 쓰레드가 Object 객체에 대한 nofity()나 nofityAll()을 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다.
17. Object 클래스에 선언되 notify() 메소드의 용도는?
wait() 메소드로 대기 중인 쓰레드를 작업을 하도록 깨워주는 역할을 한다.
18. ThreadGroup 클래스에 선언된 enumerate() 메소드의 용도는?
해당 쓰레드 그룹에 포함된 쓰레드나 쓰레드 그룹의 목록을 매개변수로 넘어온 배열에 담는다.
'도서 > 자바의 신' 카테고리의 다른 글
[도서/자바의 신] #27 Serialiable과 NIO도 살펴봅시다 (1) | 2023.01.01 |
---|---|
[도서/자바의 신] #26 파일에 있는 것을 읽고 쓰려면 아이오를 알아야죠 (1) | 2023.01.01 |
[도서/자바의 신] #24 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part 3(Map) (1) | 2022.12.30 |
[도서/자바의 신] #23 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part 2(Set과 Queue) (0) | 2022.12.30 |
[도서/자바의 신] #22 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part1(List) (1) | 2022.12.30 |