Java Thread에 대해 알아보도록 하겠습니다.
start() 메서드를 통해 실행되면, JVM은 메모리 내에 해당 스레드만을 위한 독립적인 호출 스택을 새로 생성한다. 이 스택은 각 스레드가 다른 작업 흐름과 섞이지 않고 자신만의 지역 변수와 메서드 호출 정보를 보관하는 전용 공간이다.Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하는 두 가지 방법이 있다.
Thread를 상속 받는 방법Thread 클래스를 상속받아 새로운 클래스를 만들고, run() 메서드를 오버라이딩하여 스레드가 수행할 작업을 정의한다.
// 1. Thread 클래스 상속
class MyThread extends Thread {
@Override
public void run() {
// 스레드가 수행할 작업 정의
for (int i = 0; i < 5; i++) {
// 현재 실행 중인 스레드의 이름을 가져옴
System.out.println(getName() + " 실행 중: " + i);
try {
// 0.2초간 일시 정지
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 실행 코드
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
// start()를 호출해야 새로운 호출 스택이 생성됨
t1.start();
}
}
Runnable 인터페이스를 구현하는 방법자바는 다중 상속을 지원하지 않기 때문에, 다른 클래스를 상속받아야 하는 경우 이 방법을 주로 사용한다. 인터페이스를 구현한 객체를 Thread 생성자의 매개변수로 전달해야 한다.
// 1. Runnable 인터페이스 구현
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Runnable 구현 시 Thread.currentThread()로 접근
System.out.println(Thread.currentThread().getName() + " 작업 중");
try {
Thread.sleep(200);
} catch (InterruptedException e) {}
}
}
}
// 실행 코드
public class Main {
public static void main(String[] args) {
// Runnable 객체 생성
Runnable r = new MyRunnable();
// Thread 생성자에 전달
Thread t2 = new Thread(r);
t2.start();
}
}
별도의 클래스 파일을 만들지 않고, 소스 코드 내에서 즉석으로 스레드를 생성할 때 많이 사용되는 방식이다.
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("익명 클래스로 생성된 스레드 실행");
}
});
t3.start();
참고
자바 8, 11, 그리고 최신 버전으로 올수록 단순한 new Thread().start() 방식보다는 쓰레드 풀(ThreadPool)을 관리하는 ExecutorService를 사용하는 것이 권장된다.
Callable과 Runnable에 대한 차이 참고 블로그
start() vs run()스레드를 실행할 때는 반드시 start() 메서드를 호출해야 한다. start()는 내부적으로 새로운 호출 스택(Call Stack)을 생성하여 독립적인 작업 환경을 구축한 뒤 그 위에서 run()을 실행시킨다. run()을 직접 호출하면 새로운 스레드가 만들어지지 않고 기존 스택에서 일반 메서드처럼 동작한다.
run() 메서드를 직접 호출할 경우 (일반 메서드 호출)run()을 직접 호출하면 새로운 스레드가 생성되지 않고, 현재 코드를 실행 중인 스레드(주로 메인 스레드)의 호출 스택 위에서 단순히 메서드 내용이 실행된다.class MyThread extends Thread {
public void run() {
// 현재 실행 중인 스레드의 이름을 출력
System.out.println(Thread.currentThread().getName() + "가 run()을 실행 중");
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.run(); // start()가 아닌 run()을 직접 호출
}
}
main가 run()을 실행 중이라고 출력된다.run() 메서드가 올라가 실행된 것이다. 이는 멀티 스레딩이 아닌 일반적인 객체의 메서드 호출과 동일하다.start() 메서드를 호출할 경우 (멀티 스레딩 시작)start()를 호출하면 JVM은 내부적으로 새로운 호출 스택을 생성하고, 그 독립적인 공간 위에 run() 메서드를 올려 실행시킨다. 하나의 스레드 객체에 대해 start()는 단 한 번만 호출 가능하다. 다시 실행하려면 새 객체를 만들어야 한다.public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // start() 호출
System.out.println(Thread.currentThread().getName() + "는 자기 할 일 계속함");
}
}
Thread-0가 run()을 실행 중과 main는 자기 할 일 계속함이 동시에(혹은 순차적으로) 출력된다.start()는 스레드가 독립적으로 작업을 수행할 수 있도록 메모리를 할당받고 분가하는 과정을 거친다. 이 과정에서 새로운 호출 스택이 만들어졌기 때문에 메인 스레드와 별개로 작업이 진행될 수 있는 것이다.참고
자바에서
start()메서드가 호출 스택을 만드는 원리
1. 새로운 호출 스택 생성:start()메서드를 호출하면 JVM은 해당 스레드만을 위한 독립적인 호출 스택(Call Stack)을 새로 생성한다. 이는 스레드가 다른 작업 흐름과 섞이지 않고 자신만의 전용 작업 공간을 갖게 됨을 의미한다.
2. 사전 준비 및 메모리 할당: 스레드가 CPU를 사용하기 위해서는 메모리 할당 등 독립적인 실행을 위한 사전 준비 과정이 필요하다.start()메서드는 내부적으로 이러한 준비 작업을 수행하여 스레드가 CPU와 직접 통신할 수 있는 환경을 구축한다.
3. run() 메서드 배치 및 실행: 호출 스택이 성공적으로 생성되고 준비가 완료되면, JVM은 새로 만든 스택의 가장 위에 사용자가 정의한 run() 메서드를 올린다. 이때부터 비로소 스레드는 독립적인 일꾼으로서 자신의 로직을 수행하기 시작한다.
4. 독립적 실행 흐름 확보: 이 원리를 통해 새로운 스레드는 메인 스레드나 기존의 실행 흐름에서 벗어나 분가하는 것과 같은 독립성을 얻게 된다. 따라서 각 스레드는 자신만의 호출 스택 내에 있는 지역 변수 등을 사용하여 다른 스레드와 간섭 없이 작업을 진행할 수 있다.
스레드를 관리하기 위해서는 먼저 실행 중인 스레드 객체에 접근할 수 있어야 한다.
currentThread()): 정적 메서드인 Thread.currentThread()를 사용하면 현재 코드를 실행 중인 스레드의 참조값을 얻을 수 있다. 이는 개발자가 직접 생성하지 않은 메인 스레드의 정보를 가져오거나, 참조 변수가 없는 익명 스레드의 상태를 확인해야 할 때 필수적이다.setName, getName): 모든 스레드는 이름을 가진다. 이름을 직접 지정하지 않으면 컴파일러가 "Thread-0", "Thread-1"과 같이 숫자를 붙여 자동으로 명명한다. 스레드에 이름을 부여하는 것은 멀티 스레드 환경에서 어떤 일꾼이 어떤 작업을 수행하는지 식별하고 디버깅하는 데 매우 유용하다.activeCount)Thread.activeCount() 메서드는 현재 스레드가 속한 스레드 그룹 내에서 실행 중인 스레드의 개수를 반환한다.Terminated)되면 카운트에서 제외되므로, 프로그램 실행 도중 작업의 진행 상황에 따라 이 수치는 계속 변하게 된다.우선순위는 스레드가 CPU 자원을 얼마나 더 많이 할당받을지를 결정하는 척도이다.
MIN_PRIORITY(1), NORM_PRIORITY(5), MAX_PRIORITY(10)와 같은 상수를 제공한다.Daemon Thread) 설정스레드는 성격에 따라 일반 스레드와 데몬 스레드로 나뉜다.
setDaemon(true)를 통해 설정하며, 반드시 스레드가 시작(start())되기 전에 지정해야 한다.이러한 속성들은 자바가 복잡한 멀티 태스킹 환경을 제어하는 핵심 수단이다.
결론적으로 자바 스레드 속성은 개별 스레드의 정체성(이름), 중요도(우선순위), 그리고 생사 여탈권(데몬 설정)을 정의함으로써, 개발자가 복잡한 병렬 처리 시스템을 정교하게 제어할 수 있게 돕는다.
sleep(): 지정된 시간 동안 현재 스레드를 일시 정지시킨다.join(): 다른 스레드의 작업이 끝날 때까지 기다린다.interrupt(): 일시 정지 중인 스레드를 깨워 실행 대기 상태로 만든다.yield(): 자신의 실행 시간을 다른 스레드에게 양보한다.여러 스레드가 하나의 객체(공유 자원)에 동시에 접근하여 데이터를 수정할 때 발생하는 데이터 오염을 방지하기 위해 필요하다.
한 번에 하나의 스레드만 진입할 수 있도록 설정된 코드 영역이다. synchronized 키워드를 메서드나 블록에 사용하여 설정하며, 해당 객체의 락(Lock)을 획득한 스레드만 접근할 수 있다.
wait, notify)스레드가 당장 작업을 진행할 수 없을 때 wait()를 통해 락을 반납하고 대기실로 갔다가, 조건이 충족되면 notify()나 notifyAll()을 통해 다시 깨어나 작업을 재개함으로써 실행 효율을 높인다.
JVM은 사용자가 직접 코딩하지 않아도 시스템 운영을 위해 필수적인 스레드들을 스스로 생성한다.
main() 메서드를 실행한다.참고
시스템 스레드가 활성화되거나 동작하는 시점
1) 프로그램 시작 시 (JVM 구동 시점)
자바 프로그램이 실행되면 JVM은 사용자가 별도의 코드를 작성하지 않아도 가장 먼저 메인 스레드(Main Thread)를 생성하여main()메서드를 실행한다. 이와 동시에 JVM은 환경을 유지하기 위해 자체적으로 여러 관리용 스레드들을 함께 돌리기 시작한다. 개발자가 인식하지 못할 뿐, 프로그램 시작과 동시에 이미 여러 시스템 스레드가 배경에서 작동하고 있다.
2) 메모리 관리가 필요한 시점 (가비지 컬렉터)
가장 대표적인 시스템 스레드는 가비지 컬렉터(GC, Garbage Collector)이다. 하지만 모든 GC 방식이 하나의 스레드로만 작동하는 것은 아니다. 최신 JVM에서는 여러 개의 스레드가 동시에 청소하는 Parallel GC나 G1 GC 등을 사용하므로, "GC 작업을 수행하는 스레드들은 데몬 스레드들이다"라고 이해하면 더 완벽하다.
- 활성화 조건: 가비지 컬렉터는 메모리가 부족하거나 특정 사용량(예: 60% 이상 사용)에 도달했을 때 JVM에 의해 깨어나 활동을 시작한다.
- 역할: 더 이상 사용되지 않는 메모리(쓰레기)를 정리하여 시스템이 원활하게 돌아가도록 돕는 '청소부' 역할을 수행한다.
3) 보조적인 작업이 필요한 시점 (데몬 스레드 형태)
많은 시스템 스레드는 데몬 스레드(Daemon Thread)의 속성을 가지고 동작한다.
- 작동 시점: 주 작업(일반 스레드)이 진행되는 동안 배경에서 자동 저장, 화면 재생산, 가비지 컬렉션 등 보조적인 일을 처리해야 할 때마다 작동한다.
- 종료 시점: 시스템 스레드는 오직 일반 스레드를 돕기 위해 존재하므로, 모든 일반 스레드가 종료되는 순간 자신의 작업 완료 여부와 상관없이 JVM에 의해 자동으로 함께 종료된다.
4) 운영체제(OS)의 스케줄링에 따름
시스템 스레드가 실제로 CPU를 점유하여 일을 하는 구체적인 타이밍은 OS의 스케줄러가 결정한다. OS는 여러 프로세스와 스레드 사이에서 공평하게 자원을 배분하며, 시스템 운영에 필요한 스레드들을 적절한 시점에 교체하며 실행시킨다.
가비지 컬렉터는 자바에서 데몬 스레드(Daemon Thread)로 구현되어 동작한다. 데몬 스레드는 주 작업 스레드(일반 스레드)를 돕는 조수 역할을 하므로, 모든 일반 스레드가 종료되면 가비지 컬렉터 스레드도 자신의 작업 완료 여부와 상관없이 자동으로 함께 종료된다.
join()처럼 코드로 기다리는 게 아니라, JVM이 안전한 메모리 정리를 위해 강제로 멈추는 것이다. GC 작업이 끝나면 다시 스레드들을 움직이게 한다.가비지 컬렉션은 시스템 자원을 효율적으로 사용하기 위해 필수적이지만, 멀티 스레드 환경에서 적절히 제어되지 않으면 프로그램의 응답성에 영향을 줄 수 있다. 소스에 따르면 가비지 컬렉션과 같은 보조 작업이 너무 빈번하거나 적절한 시간 간격 없이 실행되면 메인 작업 스레드의 흐름이 방해받을 수 있다.