스레드(Thread)는 코드의 실행 흐름을 나타냅니다. 하나의 프로그램은 일반적으로 하나 이상의 스레드를 가지며, 각 스레드는 프로그램의 일부분을 동시에 실행합니다. 스레드를 사용하면 여러 작업을 동시에 처리하거나, 여러 작업을 병렬로 실행할 수 있습니다.
스레드는 독립적인 실행 경로를 가지며, 각각의 스레드는 자신만의 스택을 가지고 있습니다. 이를 통해 각 스레드는 독립적으로 메서드를 호출하고, 변수를 사용할 수 있습니다. 또한, 다른 스레드와 데이터를 공유하면서 동시에 작업을 수행할 수도 있습니다.
Java에서는 java.lang.Thread 클래스를 사용하여 스레드를 생성하고 관리할 수 있습니다. 또한, java.util.concurrent 패키지를 사용하여 스레드 풀을 생성하고 다양한 동시성 작업을 처리할 수도 있습니다.
스레드를 사용하면 다음과 같은 작업을 수행할 수 있습니다.
스레드를 사용하면 프로그램의 성능을 향상시키고, 사용자 경험을 개선할 수 있으나, 잘못된 사용은 동기화 문제나 데드락과 같은 문제를 발생시킬 수 있으므로 주의가 필요합니다.
멀티 스레드 환경에서는 하나의 스레드에서 발생한 예외가 적절하게 처리되지 않으면 프로세스 전체에 영향을 미칠 수 있습니다. 이는 예외가 발생한 스레드에서 예외 처리가 제대로 이루어지지 않으면 해당 스레드가 종료되고, 종료되지 않은 예외는 상위 호출 스택으로 전파됩니다.
만약 상위 호출 스택에서도 예외가 적절하게 처리되지 않으면, 해당 예외는 최종적으로 JVM(Java Virtual Machine)에 의해 처리되지 않은 예외(Uncaught Exception)로 간주되어 프로세스가 종료될 수 있습니다. 따라서 멀티 스레드 환경에서는 모든 스레드에서 예외 처리가 중요하며, 적절한 예외 처리를 통해 안정성을 확보해야 합니다.
그러므로 멀티 스레드 환경에서는 각각의 스레드에서 예외 처리를 적절히 해주어야 하며, 특히 메인 스레드에서 생성된 서브 스레드의 경우에는 예외 처리에 더욱 주의를 기울여야 합니다.
작업 스레드를 직접 생성하는 방법에는 크게 두 가지가 있습니다.
Thread
클래스를 상속받아 새로운 클래스를 정의하는 방법Runnable
인터페이스를 구현하는 방법첫 번째 방법은 Thread
클래스를 상속받아 새로운 클래스를 정의하고, run()
메서드를 오버라이딩하여 스레드의 실행 로직을 구현하는 것입니다. 이후에 해당 클래스의 객체를 생성하여 start()
메서드를 호출하여 새로운 스레드를 시작할 수 있습니다.
두 번째 방법은 Runnable
인터페이스를 구현하는 클래스를 정의하고, run()
메서드에 스레드의 실행 로직을 구현하는 것입니다. 그리고 이를 Thread
클래스의 생성자에 전달하여 새로운 스레드를 생성하고 시작할 수 있습니다.
아래는 각각의 방법으로 작업 스레드를 직접 생성하는 예시 코드입니다:
Thread
클래스를 상속받는 방법:class MyThread extends Thread {
public void run() {
// 스레드의 실행 로직 구현
System.out.println("MyThread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 새로운 스레드 시작
}
}
Runnable
인터페이스를 구현하는 방법:class MyRunnable implements Runnable {
public void run() {
// 스레드의 실행 로직 구현
System.out.println("MyRunnable is running.");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 새로운 스레드 시작
}
}
작업 스레드를 실행하려면 스레드 객체의 start()
메서드를 호출해야 합니다. 이 메서드를 호출하면 새로운 스레드가 생성되고, 해당 스레드에서 run()
메서드가 실행됩니다.
start()
메서드는 새로운 스레드를 생성하고 스케줄링을 관리하는 역할을 합니다. 이 메서드를 호출하면 새로운 스레드가 JVM에 의해 스케줄링되어 실행될 때까지 기다리는 대신, 호출된 스레드에서 run()
메서드를 호출하여 새로운 스레드를 실행시킵니다.
class MyThread extends Thread {
public void run() {
// 스레드의 실행 로직 구현
System.out.println("MyThread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 새로운 스레드 시작
}
}
작업 스레드를 생성하는 또 다른 방법은 Thread
클래스의 서브 클래스를 만드는 것입니다. 이 방법은 Thread
클래스를 상속받아 새로운 클래스를 정의하고, 이 클래스에서 run()
메서드를 오버라이드하여 스레드의 실행 로직을 구현하는 것입니다.
class MyThread extends Thread {
public void run() {
// 스레드의 실행 로직 구현
System.out.println("MyThread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 새로운 스레드 시작
}
}
작업 스레드의 이름을 변경하고자 할 때는 setName()
메서드를 사용할 수 있습니다. 이 메서드를 사용하여 스레드의 이름을 원하는 이름으로 설정할 수 있습니다.
class MyThread extends Thread {
public void run() {
// 스레드의 실행 로직 구현
System.out.println("MyThread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("CustomThread"); // 스레드의 이름을 변경
myThread.start(); // 새로운 스레드 시작
}
}
setName()
메서드를 사용하여 작업 스레드의 이름을 "CustomThread"로 변경하고 있습니다. 이제 해당 스레드가 실행될 때는 지정된 이름으로 표시됩니다.
스레드의 이름을 변경하는 것은 스레드를 식별하거나 디버깅할 때 유용합니다. 특히 여러 개의 스레드가 동시에 실행되고 있을 때 각 스레드를 식별하기 쉽게 하기 위해 스레드의 이름을 명확하게 설정하는 것이 중요합니다.
현재 실행 중인 스레드를 확인하려면 currentThread()
메서드를 사용하여 현재 스레드의 참조를 얻은 다음, getName()
메서드를 호출하여 해당 스레드의 이름을 확인할 수 있습니다.
public class Main {
public static void main(String[] args) {
// 현재 실행 중인 스레드의 이름 확인
Thread currentThread = Thread.currentThread();
String threadName = currentThread.getName();
System.out.println("현재 실행 중인 스레드의 이름: " + threadName);
}
}
위의 예시에서 currentThread()
메서드를 사용하여 현재 실행 중인 스레드의 참조를 얻은 다음, getName()
메서드를 호출하여 해당 스레드의 이름을 가져와 출력하고 있습니다.
실행 대기 상태(Runnable):
start()
메서드가 호출되면 실행 대기 상태가 됩니다.실행 상태(Running):
run()
메서드를 실행하며, 스레드가 run()
메서드를 실행하는 동안에만 실행 상태에 있습니다.일시 중지 상태(Blocked 또는 Waiting):
일시 중단 상태(Suspended):
suspend()
메서드를 호출하여 스레드를 일시 중단시킬 수 있습니다.실행 중인 스레드를 일정 시간 동안 멈추게 하려면 sleep()
메서드를 사용할 수 있습니다. 이 메서드는 현재 실행 중인 스레드를 주어진 시간(밀리초 단위) 동안 일시 중단시킵니다.
try {
Thread.sleep(milliseconds); // milliseconds 동안 스레드 일시 중단
} catch (InterruptedException e) {
// InterruptedException 처리
}
여기서 milliseconds
는 스레드를 일시 중단할 시간을 밀리초 단위로 지정합니다. 예를 들어, 1000밀리초는 1초를 의미합니다.
public class Main {
public static void main(String[] args) {
System.out.println("스레드 시작");
try {
// 3초 동안 스레드 일시 중단
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("3초 후 스레드 종료");
}
}
sleep()
메서드를 사용하면 특정 시간 동안 스레드를 멈출 수 있으며, 이는 스레드의 동작을 제어하는 데 유용합니다. 그러나 sleep()
메서드는 예외 처리가 필요하므로, InterruptedException
을 처리하는 것을 잊지 말아야 합니다.
다른 스레드가 종료될 때까지 기다렸다가 실행을 계속하려면 join()
메서드를 사용할 수 있습니다. join()
메서드는 현재 스레드가 대기하고 있는 스레드가 종료될 때까지 대기하도록 만듭니다.
try {
thread.join(); // thread 스레드가 종료될 때까지 대기
} catch (InterruptedException e) {
// InterruptedException 처리
}
여기서 thread
는 현재 스레드가 대기할 대상 스레드를 나타냅니다. 이 메서드를 호출하면 현재 스레드는 대상 스레드가 종료될 때까지 대기하게 됩니다.
다음은 join()
메서드를 사용하여 다른 스레드가 종료될 때까지 대기하는 예시입니다:
public class Main {
public static void main(String[] args) {
Thread otherThread = new Thread(new OtherRunnable());
otherThread.start(); // 다른 스레드 시작
try {
// 다른 스레드가 종료될 때까지 대기
otherThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("다른 스레드가 종료되었습니다.");
}
}
class OtherRunnable implements Runnable {
public void run() {
System.out.println("다른 스레드가 실행 중입니다.");
try {
// 3초 동안 스레드 일시 중단
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("다른 스레드가 종료되었습니다.");
}
}
main
메서드에서 새로운 스레드를 생성하고 시작한 후에 join()
메서드를 호출하여 다른 스레드가 종료될 때까지 대기합니다. 그리고 다른 스레드가 종료되면 "다른 스레드가 종료되었습니다."라는 메시지가 출력됩니다.
이러한 방식으로 join()
메서드를 사용하여 다른 스레드의 종료를 기다릴 수 있습니다.
yield()
메서드는 현재 실행 중인 스레드가 다른 스레드에게 CPU 사용권을 양보하고 실행 대기 상태로 돌아가도록 합니다. 이렇게 함으로써 다른 스레드가 실행 상태로 전환될 기회를 얻을 수 있습니다.
Thread.yield();
이 메서드를 호출하면 현재 실행 중인 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태로 전환됩니다.
여러 스레드가 동시에 공유하는 객체를 변경할 때 발생할 수 있는 문제를 방지하기 위해 객체에 잠금을 걸어 다른 스레드가 동시에 해당 객체를 변경하지 못하도록 할 수 있습니다. 이를 위해 Java에서는 동기화 메커니즘을 제공합니다.
synchronized
키워드를 사용하면 메서드나 블록을 동기화할 수 있습니다. 이를 통해 한 번에 하나의 스레드만이 해당 메서드나 블록을 실행할 수 있도록 보장할 수 있습니다.
public class SharedObject {
private int value;
public synchronized void setValue(int newValue) {
this.value = newValue;
}
public synchronized int getValue() {
return this.value;
}
}
위의 예시에서 setValue()
와 getValue()
메서드에 synchronized
키워드가 붙어있습니다. 이는 이 메서드들이 한 번에 하나의 스레드만이 호출할 수 있도록 보장합니다.
따라서 여러 스레드가 동시에 setValue()
또는 getValue()
메서드를 호출할 때는 한 번에 하나의 스레드만이 해당 메서드를 실행하게 되어 객체에 대한 안전한 동시 접근이 보장됩니다.
이러한 방식으로 동기화를 통해 객체에 잠금을 걸면 다른 스레드가 변경할 수 없도록 할 수 있습니다.
public class Main {
public static void main(String[] args) {
SharedObject sharedObject = new SharedObject();
Thread thread1 = new Thread(new Task(sharedObject, "Thread 1"));
Thread thread2 = new Thread(new Task(sharedObject, "Thread 2"));
thread1.start();
thread2.start();
}
}
class SharedObject {
private int count = 1;
public synchronized void print(String threadName) {
while (count <= 10) {
if (threadName.equals("Thread 1") && count % 2 == 1) {
System.out.println(threadName + ": " + count++);
notify();
} else if (threadName.equals("Thread 2") && count % 2 == 0) {
System.out.println(threadName + ": " + count++);
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Task implements Runnable {
private SharedObject sharedObject;
private String threadName;
public Task(SharedObject sharedObject, String threadName) {
this.sharedObject = sharedObject;
this.threadName = threadName;
}
@Override
public void run() {
sharedObject.print(threadName);
}
}
interrupt()
메서드는 스레드의 작업을 중단시키고, 해당 스레드가 일시 정지 상태에 있을 때 InterruptedException
예외를 발생시킵니다. 즉, 이 메서드를 호출하면 스레드가 현재 일시 정지 상태에 있을 경우, 일시 정지 상태를 벗어나고 InterruptedException
예외를 발생시킵니다.
interrupt()
메서드는 스레드에게 작업을 중단하도록 신호를 보내는 역할을 합니다. 이에 대한 실제 처리는 스레드 내부의 코드에서 이를 확인하고 적절히 처리해주어야 합니다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 새로운 스레드 시작
// 일시 정지 후 2초 후에 스레드를 중단하고자 함
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 스레드 중단 요청
}
}
class MyRunnable implements Runnable {
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("스레드 실행 중...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// InterruptedException이 발생하면 스레드가 중단됨
System.out.println("스레드가 중단되었습니다.");
}
}
}
위의 예시에서 MyRunnable
클래스는 Runnable
인터페이스를 구현하여 스레드 내에서 실행됩니다. 스레드는 실행 중에 interrupt()
메서드가 호출되면 InterruptedException
을 발생시키고, 이를 catch하여 스레드가 중단되도록 처리합니다.
데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드입니다. 주로 백그라운드에서 동작하여 주 스레드의 보조 역할을 하거나 특정 작업을 주기적으로 수행하는 등의 역할을 맡습니다.
주 스레드가 종료되면 데몬 스레드는 자동으로 종료됩니다. 이는 주 스레드가 실행 중인 동안에만 유효하며, 주 스레드가 종료되면 데몬 스레드도 함께 종료됩니다.
데몬 스레드를 생성하기 위해서는 해당 스레드를 생성한 후 setDaemon(true)
메서드를 호출하여 해당 스레드를 데몬 스레드로 지정해야 합니다.
public class Main {
public static void main(String[] args) {
Thread daemonThread = new Thread(new DaemonTask());
daemonThread.setDaemon(true); // 데몬 스레드로 설정
daemonThread.start(); // 스레드 시작
// 주 스레드의 작업
for (int i = 0; i < 5; i++) {
System.out.println("Main thread executing...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class DaemonTask implements Runnable {
public void run() {
while (true) {
System.out.println("Daemon thread executing...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
DaemonTask
클래스는 Runnable
인터페이스를 구현하여 스레드 내에서 실행됩니다. setDaemon(true)
메서드를 호출하여 해당 스레드를 데몬 스레드로 설정하고, 이 스레드는 주 스레드의 보조 역할을 수행합니다. 주 스레드가 종료되면 데몬 스레드도 함께 종료됩니다.
병렬 작업이 많아지면 스레드의 개수가 폭증하여 CPU 자원을 과도하게 사용하고, 메모리 사용량이 늘어나는 문제가 발생할 수 있습니다. 이를 해결하기 위해 스레드 풀(Thread Pool)을 사용하는 것이 좋습니다.
스레드 풀은 일정한 크기의 스레드 집합을 관리하고, 필요에 따라 스레드를 재사용함으로써 스레드 생성 및 소멸에 따른 오버헤드를 줄이는데 도움을 줍니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 스레드 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(5); // 최대 5개의 스레드를 갖는 스레드 풀 생성
// 작업 제출
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("Task " + (i + 1));
executor.execute(worker); // 작업을 스레드 풀에 제출
}
// 스레드 풀 종료
executor.shutdown(); // 모든 작업이 완료된 후에 스레드 풀 종료
while (!executor.isTerminated()) {
// 스레드 풀이 종료될 때까지 대기
}
System.out.println("모든 작업이 완료되었습니다.");
}
}
class WorkerThread implements Runnable {
private String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " is executing " + taskName);
try {
Thread.sleep(1000); // 일부러 작업을 1초 동안 수행하는 것으로 가정
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has completed " + taskName);
}
}
ExecutorService
를 사용하여 스레드 풀을 생성하고, 작업을 제출하여 병렬 처리합니다. 스레드 풀에는 최대 5개의 스레드가 있으며, 10개의 작업을 실행합니다. 모든 작업이 완료된 후에는 스레드 풀을 종료합니다.
이러한 방식으로 스레드 풀을 사용하면 병렬 작업을 효율적으로 처리할 수 있으며, 스레드의 폭증을 막을 수 있습니다.
스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 메인 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있습니다. 이는 스레드 풀의 스레드가 메인 스레드에 의존하지 않고, 독립적으로 실행되기 때문입니다.
따라서 스레드 풀을 사용한 프로그램에서는 메인 스레드가 종료되더라도 스레드 풀의 스레드가 여전히 실행 중인 상태가 됩니다. 이 때, 스레드 풀의 모든 스레드를 종료하려면 shutdown()
메서드를 사용합니다.
executor.shutdown();
runnable callable의 차이점은 작업 처리 완료후 리턴값이 있느냐 없느냐이다.
Runnable 예제:
public class RunnableExample {
public static void main(String[] args) {
// Runnable 객체 생성
RunnableTask task1 = new RunnableTask("Task 1");
RunnableTask task2 = new RunnableTask("Task 2");
// 스레드 생성 및 실행
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
// Runnable 인터페이스를 구현하는 클래스
class RunnableTask implements Runnable {
private String taskName;
public RunnableTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("Task " + taskName + " is running.");
}
}
Callable 예제:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableExample {
public static void main(String[] args) {
// Callable 객체 생성
CallableTask task1 = new CallableTask("Task 1");
CallableTask task2 = new CallableTask("Task 2");
// FutureTask 객체 생성
FutureTask<String> futureTask1 = new FutureTask<>(task1);
FutureTask<String> futureTask2 = new FutureTask<>(task2);
// 스레드 생성 및 실행
Thread thread1 = new Thread(futureTask1);
Thread thread2 = new Thread(futureTask2);
thread1.start();
thread2.start();
// 결과 확인
try {
String result1 = futureTask1.get(); // 작업 결과를 얻음
System.out.println("Result of Task 1: " + result1);
String result2 = futureTask2.get(); // 작업 결과를 얻음
System.out.println("Result of Task 2: " + result2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
// Callable 인터페이스를 구현하는 클래스
class CallableTask implements Callable<String> {
private String taskName;
public CallableTask(String taskName) {
this.taskName = taskName;
}
@Override
public String call() throws Exception {
// 작업 수행
return "Result of " + taskName;
}
}