Java를 학습하는 과정에서 스레드 개념은 반드시 마주치게 된다. Android 개발을 접해보면 스레드가 얼마나 중요한지 실감나게 된다. 이번 글에서는 Java 스레드의 기본적인 사용법을 정리해보고자 한다.
프로세스와 스레드의 차이점
먼저 중요한 개념 정리가 필요하다. 프로세스(Process)와 스레드(Thread)는 서로 다른 개념이다. 프로세스는 실행 중인 프로그램을 의미하며, JVM이 class 파일을 실행할 때 생성된다. 반면 스레드는 실제로 CPU 자원을 사용하여 명령어를 실행하는 단위다. 하나의 프로세스에는 여러 스레드가 존재할 수 있으며, 각 스레드는 독립적으로 실행된다. 이러한 실행 방식을 비동기(Asynchronous) 실행이라고 한다.스레드 생성 방법
Java에서 스레드를 생성하는 가장 기본적인 방법은 Runnable 인터페이스를 구현하는 것이다.public class ThreadDemo {
public static void main(String[] args) {
Task worker = new Task();
Thread thread = new Thread(worker);
thread.start();
for (int i = 1; i <= 20; i++) {
System.out.println("메인 스레드: " + i);
}
}
}
class Task implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println("작업 스레드: " + i);
}
}
}
위의 예제에서 알 수 있듯이, Runnable 인터페이스를 구현한 클래스의 run() 메서드에 실행したい 코드를 작성한다. thread.start()를 호출하면 JVM이 새로운 스레드를 생성하고 run() 메서드를 실행한다. 직접 run() 메서드를 호출하면 단순한 메서드 호출에 지나지 않으므로 반드시 start() 메서드를 사용해야 한다.
Thread.sleep()을 활용한 스레드 제어
스레드를 일시적으로 정지시키고 싶을 때는 Thread.sleep() 메서드를 사용한다. 이 메서드는 InterruptedException을 발생시키므로 try-catch 블록으로 감싸야 한다.public class SleepDemo {
public static void main(String[] args) {
CountdownTask task = new CountdownTask();
Thread worker = new Thread(task);
worker.start();
try {
Thread.sleep(5000);
worker.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class CountdownTask implements Runnable {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
try {
Thread.sleep(1000);
System.out.println(new Date());
} catch (InterruptedException e) {
System.out.println("스레드가 중단되었습니다");
return;
}
}
}
}
interrupt() 메서드는 스레드의 실행을 중단시킨다. 하지만 deprecated된 stop() 메서드는 사용하지 않는 것이 좋다.stop()은 스레드를 강제로 종료시키므로 데이터 무결성이나 리소스 정리와 같은 정리 작업을 수행할 수 없다.
스레드를 안전하게 종료하는 방법
보다 안전한 스레드 종료 방법은 플래그 변수를 사용하는 것이다. 이 방법하면 스레드가 현재 실행 중인 작업을 완료한 후 스스로 종료할 수 있다.public class SafeStopDemo {
public static void main(String[] args) {
ControlledTask task = new ControlledTask();
Thread worker = new Thread(task);
worker.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.stopRunning();
}
}
class ControlledTask implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
int counter = 0;
while (running) {
try {
System.out.println("작업 실행 중: " + (counter++));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("스레드가 종료되었습니다");
}
public void stopRunning() {
running = false;
}
}
volatile 키워드는 여러 스레드가 공유하는 변수의 가시성을 보장한다. 이 방법을 사용하면 스레드가 갑자기 중단되는 것을 방지하고, 필요한 정리 작업을 안전하게 수행할 수 있다.
스레드 병합: join() 메서드
join() 메서드는 다른 스레드의 실행이 완료될 때까지 현재 스레드를 대기시킨다. 주로 특정 작업이 완료된 후에 후속 작업을 실행해야 할 때 사용한다.public class JoinDemo {
public static void main(String[] args) {
SequenceTask task = new SequenceTask();
Thread worker = new Thread(task);
worker.start();
try {
worker.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("메인 스레드: " + i);
}
}
}
class SequenceTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("작업 스레드: " + i);
}
}
}
위의 예제에서 worker.join()을 호출하면 작업 스레드가 완전히 종료된 후才会执行 메인 스레드의 반복문이 실행된다. 이를 통해 스레드 실행 순서를 제어할 수 있다.
CPU 양보: yield() 메서드
yield() 메서드는 현재 실행 중인 스레드가 CPU 사용권을 다른 스레드에게 양보하도록 한다. 이 메서드는 스레드 스케줄러에 대한 힌트를 제공할 뿐, 반드시 다른 스레드로 전환된다고 보장하지는 않는다.public class YieldDemo {
public static void main(String[] args) {
PriorityTask task = new PriorityTask("Worker");
task.start();
for (int i = 0; i <= 10; i++) {
System.out.println("메인 스레드: " + i);
}
}
}
class PriorityTask extends Thread {
PriorityTask(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
if (i % 3 == 0) {
System.out.println("작업 스레드 " + i + " - CPU 양보");
yield();
} else {
System.out.println("작업 스레드: " + i);
}
}
}
}
yield() 메서드는 Thread 클래스를 상속해야만 사용할 수 있다. 이 메서드는 주로 스레드 간의公平한 CPU 시간 분배가 필요할 때 사용한다.
스레드 우선순위
Java에서 스레드優先順位은 1에서 10까지 설정할 수 있으며, 기본값은 5이다. 우선순위가 높은 스레드가 더 많은 CPU 시간을 할당받을 가능성이 높지만, 이는 플랫폼에 따라 달라질 수 있다.public class PrioritySettingDemo {
public static void main(String[] args) {
TaskA taskA = new TaskA();
TaskB taskB = new TaskB();
taskA.start();
// taskA.setPriority(Thread.NORM_PRIORITY + 3);
taskB.start();
}
}
class TaskA extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("TaskA: " + i);
}
}
}
class TaskB extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("TaskB: " + i);
}
}
}
setPriority() 메서드로 스레드의 우선순위를 조정할 수 있다. Thread.MIN_PRIORITY(1), Thread.NORM_PRIORITY(5), Thread.MAX_PRIORITY(10)과 같은 상수를 사용할 수도 있다.