Thread 클래스와 Runnable 인터페이스
먼저 프로세스와 쓰레드가 뭔지 알아보자
Process
- 독립적인 실행 단위로, 운영체제에서 자원과 메모리를 할당받아 실행되는 프로그램의 인스턴스를 나타낸다
- 각 프로세스는 자체 메모리 공간을 가지며, 서로 간섭하지 않고 독립적으로 실행된다.
Thread
- 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다
- 모든 프로세스에는 1개 이상의 쓰레드가 존재하여 작업을 수행한다
- 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스 라고한다
Thread 클래스와 Runnalbe 인터페이스
쓰레드 생성 방법 2가지
- Runnable 인터페이스 사용
- Thread 클래스 이용
아래는 자바내에 정의된 Thread클래스의 코드다(Thread관련 메서드와 변수를 java.lang.Thread에서 제공). Runnable 인터페이스를 구현한 클래스 인 것을 알 수 있다.
따라서 Thread클래스를 쓰는것과 Runnable 인터페이스를 사용해서 구현하냐는 사용자의 선택이다.
아래는 Thread와 Runnable 두가지 방식을 통해 생성한 스레드가 실행되는 코드이다
public class ThreadRunnableEX {
public static void main(String[] args) {
// Thread를 상속받는 스레드 생성
ThreadThread thread1 = new ThreadThread(); //실행 로직을 이미 갖고있다, 따라서 바로 시작가능
// 스레드 시작
thread1.start();
// Runnable을 구현한 스레드 생성
RunnableThread runnableThread = new RunnableThread(); //스레드의 실행 로직 정의(RunnableThread 객체를 생성)
Thread thread2 = new Thread(runnableThread); //실행 로직 정의 했으므로 이제 객체 생성
// 스레드 시작
thread2.start();
}
static class ThreadThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadThread: " + i);
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class RunnableThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("RunnableThread: " + i);
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
멀티 쓰레드
2개 이상의 쓰레드가 작동하는 것을 의미한다
사실상 1개의 프로세스에서는 시간차이가 없기에 큰 의미가 없으나 2개 이상부터는 효율을 내기에 필요하다
그렇다고 좋은것만 있다고 할 수는 없다
위에 Runnable과 Thread를 통해 실행한 2개의 쓰레드의 결과를 보면 번갈아가며 작동하는것을 알 수 있다
장단점
- 장점: CPU사용률 향상, 자원 효율적 사용, 사용자 응답성 향상, 작업 분리에 따른 코드 간결
- 단점: 교착상태
Thread sleep 메서드
- sleep메소드는 주어진 시간만큼 대기를 하게된다
- sleep(long millis): millis는 초를 1000으로 쪼갠것으로 넘어온 파라미터값 (millis/1000)초 만큼 대기(millis 1000이 들어오면 1초 대기)
- sllpe(long millis, int nanos): 첫번째 파라미터 (millis/1000) + 두번째 파라미터값(1/1,000,000,000)초 만큼 대기
따라서 위에 예제를 실행하게되면 1초가 지날때마다 출력이 되는것을 알 수 있다
start()와 run()
쓰레드를 실행하기 위해서는 start메서드를 통해 해당 쓰레드를 호출해야한다. start메서드는 쓰레드가 작업을 실행할 호출 스택을 만들고 그 안에 run메서드를 올려주는 역할을 한다.
추가로 사용한 쓰레드는 재사용이 불가능하다. 다시 생성한 후에 start()호출하고 실행해야 한다.
그리고 start()를 하지 않으면 쓰레드 생성안하고 그냥 main쓰레드에서 run메서드를 2번 호출한게 되어버린다
위에 코드가 작동되기는 하지만 아래 그림처럼 쓰레드가 새로 생성되고 작동하는것이 아니라 쓰레드 생성 없이 그냥 작동만 되는것이다.
쓰레드의 상태
쓰레드의 현재 상태를 나타낸다
상태 | 의미 |
NEW | 쓰레드 객체는 생성되었지만, 아직 시작되지 않은 상태 |
RUNNABLE | 쓰레드가 실행중인 상태 |
BLOCKED | 쓰레드가 실행 중지 상태이며, 모니터 락이 풀리기를 기다리는 상태 |
WAITING | 쓰레드가 대기중인 상태 |
TIMED_WAITING | 특정 시간만큼 쓰레드가 대기중인 상태 |
TERMINATED | 쓰레드가 종료된 상태 |
쓰레드의 실행제어
메서드
- sleep: 지정된 시간동안 쓰레드를 일시정지시킨다(위에 설명)
- join: 지정된 시간동안 지정한 쓰레드가 실행되도록 한다, 지정된 시간이 지나면 다시 돌아와 계속해서 진행
- interrupt: sleep()이나 join에 의해 일시정지상태인 쓰레드를 개워서 실행대기 상태로 만든다, 해당쓰레드에서 interruptedException이 발생함으로 일시정지 상태를 벗어난다
- suspend: 쓰레드를 일시정지시킨다
- stop: 쓰레드를 즉시 종료시킨다
- resume: suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기 상태로 만든다
- yield: 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다
yield 메서드는 실행대기 상태인 쓰레드를 실행상태로 만든다
이제는 사용하지 않는 메서드
자바 문서에서도 확인 할 수 있는데 메서드 밑에 Deprecated가 명시되어있다.
사용하지 않는 이유는 스레드 관리와 동기화에 관련된 많은 문제와 예기치 못한 동작을 초래할 수 있다.
※Deprecated: 이전에는 사용했으나 더 이상 사용하지 않음
참고: https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
Deprecated Methods
- suspend(): 교착 상태가 발생하기 쉽다
- countStackFrames(): suspend()와 연관된 메서드
- destroy(): 만일 중요한 리소스를 잠금한상태로 보유하고 있으면 다시 그 리소스에 접근할 방법이 없다
- resume(): 교착상태가 발생되기에 사용안한다
- stop(): stop을 사용하면 데이터 손상에 의한 데이터 일관성 문제가 발생 할 수 있다 -> interrupt사용 권장
쓰레드의 우선순위
Java 에서 각 쓰레드는 우선순위에 관한 자신만의 필드를 가지고 있다. 이러한 우선 순위에 따라 특정 쓰레드가 더 많은 시간동안 작업할 수 있도록 설정한다
우선순위
- main() 메소드 실행 쓰레드 값: 5
- 우선순위 제일 큰값: 10
- 우선순위 제일 작은값: 1
우선순위가 높다는것은 낮은 우선순위를 가진 쓰레드보다 더 많이 실행 큐에 포함되고, 좀 더 많은 작업시간을 할당 받는다
//예시
Thread t1 = new ThreadEx_1();
t1.setPriority(1);
Thread t2 = new ThreadEx_2();
t2.setPriority(10);
t1.start();
t2.start();
※쓰레드의 우선 순위를 높이면 더 많은 시간과 실행 기회를 부여받을 수 있다. 그런데 주의할 점은 이것이 반드시 보장되는 것이 아니라는 것이다. 쓰레드의 작업할당은 OS의 스케줄링 정책과 JVM구현에 따라 다르기 때문에 코드에서 우선순위를 지정하는 것은 단지 희망사항을 전달하는 것일 뿐, 실제 작업은 설정한 우선 순위와 다르게 진행될 수 있다.
Main 쓰레드
mian 메서드도 하나의 쓰레드이다. 이를 메인 쓰레드 라고 한다. 프로그램이 시작하면 가장 먼저 실행되는 쓰레드이며, 모든 쓰레드는 메인 쓰레드로부터 생성된다(아래그림 참고). 다른 쓰레드를 생성해서 실행하지 않으면, 메인 쓰레드가 종료되는 순간 프로그램도 종료된다. 하지만 여러 쓰레드를 실행하면, 메인쓰레드가 종료되어도 다른 쓰레드가 작업을 마칠때까지 프로그램이 종료되지 않는다. 쓰레드는 '사용자 쓰레드'와 '데몬 쓰레드'로 구분되는데, 실행 중인 사용자 쓰레드가 하나도 없을때 프로그램이 종료된다.
데몬 쓰레드(Daemon Thread)
쓰레드의 종류는 일반 쓰레드와 데몬 쓰레드로 나뉜다. Main 쓰레드의 작업을 돕는 보조적인 역활을 하는 쓰레드이다. 일반 쓰레드가 종료되면 데몬 쓰레드는 강제적으로 종료된다. 주로 가비지 컬렉터, (워드 등의)자동저장, 화면 자동갱신등에 사용된다
쓰레드 그룹
서로 관련된 쓰레드는 쓰레드 그룹으로 묶어서 관리할 수 있다. 쓰레드 그룹은 다른 쓰레드 그룹을 포함시킬 수 있다. 쓰레드 그룹이 생긴이유는 보안상의 이유다. 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드는 변경할 수 없다. 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 속하며, 쓰레드 생성시 쓰레드 그룹을 지정해주지 않으면 자동적으로 main쓰레드 그룹에 속하게 된다. 아래처럼 말이다.
쓰레드를 그룹에 포함시키려면 Thread의 생성자를 이용해야 한다.
ThreadGroup 객체를 이용해서 새로운 쓰레드 그룹을 생성할 수 있다. 이때, 부모 스레드 그룹을 지정하지 않으면 현재 실행중인 쓰레드가 속한 쓰레드 그룹이 부모가 된다.
위에 코드를 보게되면 t1쓰레드는 그룹을 g1으로 설정해주어서 Group1에 포함되어있다고 뜬다. 하지만
t2는 별도로 지정을 안했기에 현재 실행중인 main쓰레드가 속한 그룹인 main그룹 에 들어가게된다.
동기화(Synchronize)
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화' 라고 한다
따라서 synchronized라는 임계 영역을 성정해서 사용하면 된다
두가지 방식
- 메서드 전체를 임계 영역으로 지정
- 특정한 영역을 임계 영역으로 지정
메서드 앞에 synchronized를 붙여 사용하면 된다. 그렇게 되면 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환한다.
하나의 쓰레드가 임계 영역을 오래 사용하는 것을 방지하고자 wait(), notify() 메서드를 사용한다.
wait() & notify()
동기화를 하게 되면 하나의 작업을 하나의 쓰레드밖에 하지 못하기 때문에 작업 효율이 떨어질 수 밖에 없다.
이때 동기화의 효율을 높이기 위해서 wait(), notify()를 이용한다
- wait(): 쓰레드가 락을 반납하고 기다리게 한다
- notify(): 작업을 중단했던 쓰레다가 다시 락을 얻어 작업을 진행할 수 있다
데드락(교착 상태, Deadlock)
둘 이상의 쓰레드가 lock을 획득하기 위해 대기하는데, 이 lock을 잡고 있는 쓰레드들도 똑같이 다른 lock을 기다리면서 서로 block상태에 놓이는 것을 말한다. 다수의 쓰레드가 같은 lock을 동시에, 다른 명령에 의해 획득하려 할 때 발생한다
예시
Thread-1이 A의 lock을 가지고 있는 상태에서 B의 lock을 획득할려한다. 동시에 Thread-2는 B의 lock을 가진상태에서 A의 lock을 획득할려한다. 이러면 서로 A,B를 가질수없는 데드락(교착) 상태가 발생한다. 그림으로 표현하면 아래와 같다.
데드락은 한 시스템 내에서 다음의 4가지 조건이 동시에 성립할때 발생한다. 아래 네 가지 조건 중 하나라도 성립하지 않도록
만든다면 교착 상태를 해결 할 수 있다.
- 상호 배제
- 자원은 한번에 한 프로세스만이 사용할 수 있어야 한다
- 점유 대기
- 최소한 하나의 자원을 점유하고 있으면 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기 하는 프로세스가 있어야 한다
- 비선점
- 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 빼앗을 수 없어야 한다
- 순환 대기
- P0 -> P1 자원 대기, P1 -> P2 자원 대기, ....Pn-1 -> Pn 자원 대기, Pn -> P0 자원 요구가 되어야 순차적으로 겹치지 않고 자원 점유가 가능하다
참고
자바의 정석(남궁성 저)
https://wisdom-and-record.tistory.com/48
오라클 공식문서
'자바(Java)' 카테고리의 다른 글
[자바] 자바 어노테이션 (1) | 2023.10.05 |
---|---|
[자바] 자바 Enum (0) | 2023.09.27 |
[자바] 자바 예외처리 (0) | 2023.09.14 |
[자바] 자바 인터페이스 (0) | 2023.09.08 |
[자바] 자바 패키지(package) (0) | 2023.08.28 |