JAVA에서 Thread 구현 방법
JAVA에서 Thread 구현 방법
JAVA에서 Thread 구현 방법
자바에서 스레드를 구현하고 실행하는 주요 방법은 크게 두 가지입니다:
- Thread 클래스 확장(Extending the Thread class): 자바에서 제공하는 Thread 클래스를 상속받아서 스레드를 구현하는 방법입니다. run 메서드를 오버라이드하여 스레드에서 실행될 로직을 정의합니다.
- class MyThread extends Thread { @Override public void run() { // 스레드에서 실행될 코드 } } // 스레드 사용 MyThread t = new MyThread(); t.start();
- Runnable 인터페이스 구현(Implementing the Runnable interface): Runnable 인터페이스를 구현하는 방법입니다. run 메서드를 구현하여 스레드에서 실행될 로직을 정의합니다. 이 방법은 클래스를 상속받아 사용하는 것보다 유연하며, 이미 다른 클래스를 상속받고 있는 경우에도 스레드를 사용할 수 있습니다.
- class MyRunnable implements Runnable { @Override public void run() { // 스레드에서 실행될 코드 } } // 스레드 사용 Thread t = new Thread(new MyRunnable()); t.start();
이 두 가지 방법 외에도, 병렬 처리와 관련된 다양한 작업들을 위해 자바에서는 Executor 프레임워크, ForkJoinPool, CompletableFuture 등의 고급 도구와 기능도 제공하고 있습니다.
개발자의 상황과 필요에 따라 적절한 방법을 선택하여 스레드를 구현하고 실행할 수 있습니다.
Thread 클래스 확장
Thread 클래스를 확장하는 방법은 스레드를 생성하고 관리하는 기본적인 방법 중 하나입니다. 다음은 이 방법에 대한 상세한 설명입니다:
- 클래스 생성:
- 먼저, Thread 클래스를 상속받는 새로운 클래스를 생성합니다.
- run 메서드 오버라이딩:
- run 메서드를 오버라이딩하여 스레드가 실행될 때 수행할 작업을 정의합니다. 이 메서드 안에 스레드의 주 실행 로직이 위치합니다.
class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 실행 중: " + i);
try {
Thread.sleep(1000); // 1초 동안 휴면
} catch (InterruptedException e) {
System.out.println("스레드가 중단되었습니다.");
}
}
}
}
- 스레드 객체 생성 및 시작:
- 위에서 정의한 클래스의 객체를 생성한 후, start 메서드를 호출하여 스레드를 시작합니다. start 메서드 호출 시, 새로운 호출 스택이 생성되고 run 메서드가 호출됩니다.
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("스레드1");
t1.start();
MyThread t2 = new MyThread();
t2.setName("스레드2");
t2.start();
}
}
- 기타 메서드:
- Thread 클래스에는 스레드의 우선순위를 설정하는 setPriority, 스레드를 일시적으로 중지시키는 sleep, 다른 스레드의 완료를 기다리는 join 등의 유용한 메서드들이 있습니다. 이들 메서드를 사용하여 스레드의 동작을 세밀하게 제어할 수 있습니다.
- 예외 처리:
- 스레드 실행 중 발생할 수 있는 예외들, 예를 들면 InterruptedException 등, 는 적절한 방법으로 처리해야 합니다.
위의 코드 예제는 두 개의 스레드를 생성하고 실행하는 간단한 예제입니다. 두 스레드는 각각 1초 간격으로 0부터 4까지의 숫자를 출력합니다.
Runnable 인터페이스 구현
Runnable 인터페이스는 스레드의 실행 코드를 정의하는 방법을 제공하는 인터페이스입니다. Thread 클래스를 확장하는 것보다 더 유연한 방법이라고 할 수 있습니다. 클래스의 상속에 제한이 있기 때문에(자바에서는 다중 상속이 불가능하므로), 이미 다른 클래스를 상속받는 경우나 다양한 인터페이스를 구현해야 하는 경우 Runnable 인터페이스 구현이 더 적합합니다.
- Runnable 인터페이스 구현:
- Runnable 인터페이스를 구현하는 클래스를 생성합니다.
- run 메서드를 오버라이드하여 스레드의 실행 코드를 정의합니다.
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 실행 중: " + i);
try {
Thread.sleep(1000); // 1초 동안 휴면
} catch (InterruptedException e) {
System.out.println("스레드가 중단되었습니다.");
}
}
}
}
- 스레드 생성 및 시작:
- Thread 클래스의 생성자에 Runnable 객체를 전달하여 스레드 객체를 생성합니다.
- 생성된 스레드 객체의 start 메서드를 호출하여 스레드를 시작합니다.
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.setName("스레드1");
t1.start();
Thread t2 = new Thread(new MyRunnable());
t2.setName("스레드2");
t2.start();
}
}
- Runnable의 장점:
- 이미 다른 클래스를 상속받는 경우에도 스레드를 사용할 수 있습니다.
- 여러 인터페이스를 구현할 수 있기 때문에, 다른 인터페이스와 함께 Runnable 인터페이스도 구현할 수 있습니다.
- 코드 재사용성과 객체 지향적 설계를 더 쉽게 할 수 있습니다.
- 기타 메서드 및 예외 처리:
- Runnable 인터페이스를 구현하는 것만으로는 Thread 클래스의 다양한 메서드들을 직접 사용할 수 없습니다. 그러나 Thread 객체를 생성할 때 Runnable 객체를 파라미터로 전달하면, 해당 Thread 객체를 통해 setPriority, sleep, join 등의 메서드를 사용할 수 있습니다.
- 스레드 실행 중 발생하는 예외 처리도 동일하게 수행할 수 있습니다.
Runnable 인터페이스를 사용하는 것은 스레드의 실행 코드만 분리하는 방법이므로, 스레드의 생명 주기나 다른 관련 기능들을 제어하기 위해서는 여전히 Thread 클래스의 인스턴스와 그 메서드들을 사용해야 합니다.
Thread 클래스
Thread 클래스
Thread 클래스는 자바에서 멀티 스레딩 프로그래밍을 지원하기 위해 java.lang 패키지에 포함되어 있습니다. 이 클래스를 사용하면 JVM 안에서 동시에 여러 스레드를 실행할 수 있습니다.
주요 생성자:
- Thread(): 기본 생성자입니다.
- Thread(String name): 스레드 이름을 지정하여 스레드를 생성합니다.
- Thread(Runnable r): Runnable 인터페이스를 구현하는 객체를 받아 스레드를 생성합니다.
- Thread(Runnable r, String name): Runnable 인터페이스를 구현하는 객체와 스레드 이름을 동시에 받아 스레드를 생성합니다.
주요 메서드:
- void start(): 스레드를 시작합니다. 이 메서드를 호출하면 JVM은 run() 메서드를 호출합니다.
- void run(): 스레드가 수행할 작업을 정의하는 곳입니다. 이 메서드를 오버라이드해서 스레드에서 실행할 코드를 정의해야 합니다.
- static void sleep(long milliseconds): 현재 실행 중인 스레드를 지정된 밀리초 동안 일시 정지시킵니다.
- void join(): 현재 스레드가 종료될 때까지 호출한 스레드를 대기 상태로 만듭니다.
- int getPriority(): 스레드의 우선순위를 반환합니다.
- void setPriority(int newPriority): 스레드의 우선순위를 설정합니다.
- static Thread currentThread(): 현재 실행 중인 스레드를 반환합니다.
- String getName(): 스레드의 이름을 반환합니다.
- void setName(String name): 스레드의 이름을 설정합니다.
- void interrupt(): 스레드에 인터럽트를 발생시킵니다.
- boolean isInterrupted(): 스레드가 인터럽트되었는지 여부를 확인합니다.
사용 예제:
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " 값: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
위의 예제에서 MyThread라는 클래스는 Thread 클래스를 확장하고, run 메서드를 오버라이드하여 원하는 작업을 정의하고 있습니다. main 메서드에서는 두 개의 스레드 객체를 생성하고, start 메서드를 호출하여 스레드를 실행하고 있습니다.
이렇게 Thread 클래스를 확장하여 멀티 스레드 프로그래밍을 할 수 있습니다.
스레드의 작업 정의로 얻는 이득
스레드의 작업 내용을 정의함으로써 얻는 주요 이점은 다음과 같습니다:
- 동시성: 여러 작업을 동시에 실행할 수 있으므로 애플리케이션의 효율성과 반응성을 향상시킬 수 있습니다. 예를 들어, UI 애플리케이션에서 사용자 입력을 처리하는 동안 다른 스레드에서 배경 작업을 실행할 수 있습니다.
- 자원 활용: 멀티 코어 또는 멀티 프로세서 시스템에서 각 코어 또는 프로세서를 효율적으로 활용할 수 있습니다. 이를 통해 애플리케이션의 전반적인 성능을 최적화할 수 있습니다.
- 비동기 처리: 네트워크 요청, 파일 I/O와 같은 긴 작업을 비동기적으로 수행하면 애플리케이션의 주 스레드가 차단되지 않습니다. 이로 인해 애플리케이션의 사용자 경험이 향상됩니다.
- 병렬 처리: 데이터 처리와 같은 병렬로 처리될 수 있는 작업을 여러 스레드로 나누어 처리하면 작업 시간을 크게 단축시킬 수 있습니다.
- 모듈성: 특정 작업을 별도의 스레드로 분리하면 코드의 구조가 명확해지고, 각 스레드가 수행하는 작업에 대한 책임이 분명해집니다.
- 경쟁 상황과 교착 상태 관리: 여러 스레드가 공유 자원에 동시에 접근하려 할 때 발생할 수 있는 문제점들을 인식하고 관리하는 기회가 제공됩니다. 이를 통해 애플리케이션의 안정성을 높일 수 있습니다.
스레드를 통한 동시성 구현은 많은 이점을 제공하지만, 동시에 고려해야 할 사항들 (공유 자원의 동기화, 교착 상태, 경쟁 상황 등)이 있습니다. 따라서 스레드를 사용할 때는 주의 깊게 설계와 구현을 해야 합니다.
데드락이 일어나는 상황이라고 가정하고 대처하는 코드
데드락은 두 개 이상의 스레드가 서로 다른 자원을 가지려고 할 때 서로 무한히 기다리는 상황을 말합니다. 이를 설명하기 위해 간단한 예제를 제시하겠습니다.
두 개의 자원, Resource1과 Resource2가 있다고 가정합니다. 두 스레드는 먼저 Resource1을 획득한 다음 Resource2를 획득하려고 시도합니다. 그러나 두 스레드가 동시에 실행되면, 한 스레드는 Resource1을 가지고 있을 때 다른 스레드가 Resource2를 가지고 있어서 데드락이 발생할 수 있습니다.
데드락 상황의 코드: (gtp4)
public class DeadlockExample {
private static final Object Resource1 = new Object();
private static final Object Resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (Resource1) {
System.out.println("Thread 1: Resource1 잠금");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
synchronized (Resource2) {
System.out.println("Thread 1: Resource2 잠금");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (Resource2) {
System.out.println("Thread 2: Resource2 잠금");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
synchronized (Resource1) {
System.out.println("Thread 2: Resource1 잠금");
}
}
});
thread1.start();
thread2.start();
}
}
이 코드에서는 두 스레드가 서로 다른 순서로 두 자원을 잠글 때 데드락이 발생할 수 있습니다.
데드락을 피하는 한 가지 방법은 자원을 항상 동일한 순서로 잠그는 것입니다. 따라서 위의 코드에서 두 스레드가 모두 Resource1을 먼저 잠그도록 수정하면 데드락을 피할 수 있습니다.
데드락 대처하는 기본적인 방법들
- 순서를 정해서 자원에 접근하기: 모든 스레드가 자원을 동일한 순서로 잠근다면 데드락은 발생하지 않습니다. 위에서 본 예제와 같이 항상 Resource1을 먼저 잠근 다음 Resource2를 잠그도록 통일한다면 데드락은 발생하지 않습니다.
- 타임아웃 사용하기: 스레드가 자원을 잠글 때 제한된 시간 내에 자원을 획득하지 못하면 대기를 중단하도록 타임아웃을 설정할 수 있습니다. 이 방식은 데드락이 발생할 가능성을 줄여주지만, 자원 경쟁이 심할 때는 효율성이 떨어질 수 있습니다.
- 데드락 감지: 시스템이 데드락을 감지하고 특정 스레드를 중단시켜 다른 스레드가 자원을 획득하게 하는 방식입니다. 이 방법은 복잡하며 데드락을 자주 발생시키는 시스템에서만 사용됩니다.
- 다시 시도: 스레드가 자원을 잠글 때 실패하면 일정 시간 후에 다시 시도합니다.
- 데드락 회피 알고리즘 사용: 특정 알고리즘을 사용하여 데드락 발생 조건을 회피하는 방법이 있습니다. 예를 들면, 뱅커스 알고리즘(Banker's algorithm)과 같은 알고리즘을 사용하여 데드락 발생을 회피할 수 있습니다.
핵심은 자원을 요청하고 잠글 때 일관된 방식을 사용하는 것입니다. 잘 설계된 프로그램에서는 스레드간에 자원 경쟁을 최소화하려고 노력하며, 필요한 경우 위의 방법 중 하나 또는 여러 가지를 조합하여 데드락을 피합니다.
synchronized 메서드
synchronized는 자바에서 동시성을 제어하기 위해 사용되는 키워드입니다. synchronized를 사용하면 여러 스레드가 동시에 해당 코드 블록이나 메서드에 접근하는 것을 제한하여, 공유된 자원에 대한 동시 접근을 막을 수 있습니다. 즉, 한 번에 하나의 스레드만이 synchronized로 표시된 메서드나 블록 내의 코드를 실행할 수 있습니다.
synchronized는 두 가지 방식으로 사용될 수 있습니다:
- 메서드 수준에서의 사용:위와 같이 메서드 선언부에 synchronized 키워드를 추가하면 해당 메서드는 한 번에 하나의 스레드만 실행할 수 있습니다. 인스턴스 메서드의 경우 해당 객체의 인스턴스에 대한 잠금을 얻게 되며, 정적 메서드의 경우 클래스 자체에 대한 잠금을 얻게 됩니다.
- public synchronized void myMethod() { // 코드... }
- 블록 수준에서의 사용:위와 같이 특정 코드 블록에 대해서만 동기화를 원할 때 사용됩니다. this는 현재 객체를 나타내며, 필요에 따라 다른 객체를 사용하여 잠금을 얻을 수도 있습니다.
- public void myMethod() { synchronized(this) { // 코드... } }
synchronized를 사용할 때 주의할 점은 데드락이 발생할 수 있다는 것입니다. 두 개 이상의 스레드가 여러 자원에 동시에 접근하려고 할 때, 서로가 필요로 하는 자원을 잠가놓고 기다리는 상황이 발생하면 데드락이 발생할 수 있습니다. 따라서 synchronized를 사용할 때는 항상 데드락을 방지하는 전략을 함께 고려해야 합니다.
Runable 인터페이스으로 구현하는 방법
1. Runnable 인터페이스란?
Runnable은 자바에서 스레드를 생성하기 위한 기본 인터페이스입니다. Runnable에는 단순하게 run() 메서드만 정의되어 있습니다. 이 run() 메서드는 스레드가 시작되었을 때 실행되는 코드를 포함합니다.
2. Runnable 사용 예제:
2.1 Runnable 구현하기:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 여기에 스레드로 실행할 작업 코드를 작성합니다.
System.out.println("Runnable을 구현한 스레드가 실행 중입니다.");
}
}
2.2 Runnable 객체를 이용하여 스레드 시작하기:
public class Main {
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
이렇게 Runnable 인터페이스를 구현한 클래스의 객체를 만든 후, Thread 클래스의 생성자에 전달하여 스레드를 생성하고 start() 메서드를 호출하여 스레드를 시작할 수 있습니다.
3. Runnable의 장점:
- 다중 상속의 문제 해결: 자바는 다중 상속을 지원하지 않습니다. 따라서, 이미 다른 클래스를 상속받는 경우 Thread 클래스를 또 상속받을 수 없습니다. 이때 Runnable 인터페이스를 사용하면 유용합니다.
- 유연성: Runnable은 기능적으로 스레드의 실행을 정의하는 것에 중점을 둡니다. 따라서 여러 스레드에서 동일한 객체의 run() 메서드를 공유할 수 있습니다.
- 재사용성: Runnable 객체는 Thread 객체와 독립적이므로, 필요에 따라 다른 Thread 객체와 함께 재사용될 수 있습니다.
Runnable을 사용하는 것은 스레드의 작업을 정의하는 데 초점을 맞추게 되므로, 코드 구조가 더 깔끔하고 관리하기 쉬워집니다. 따라서 일반적으로 스레드를 생성할 때는 Runnable 인터페이스를 구현하는 것이 권장됩니다.
Runnable 인터페이스로 스레드 구현한 동시성 문제 자바 예제 코드
Runnable 인터페이스로 스레드를 구현하든, Thread 클래스를 직접 상속받아 구현하든 동시성 문제의 해결 방법은 동일합니다. synchronized 키워드나 명시적인 락을 사용하여 동시성 문제를 해결할 수 있습니다.
아래는 Runnable 인터페이스를 사용하여 스레드를 구현하면서 동시성 문제를 해결하는 예제입니다.
public class Counter implements Runnable {
private int count = 0;
private final Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increaseCount();
}
System.out.println(Thread.currentThread().getName() + " 완료, 현재 count: " + count);
}
public void increaseCount() {
synchronized(lock) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(counter, "스레드 1");
Thread t2 = new Thread(counter, "스레드 2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("최종 count 값: " + counter.count);
}
}
위 예제에서는 Counter라는 클래스가 Runnable 인터페이스를 구현하고 있습니다. 이 클래스는 count라는 변수를 1씩 증가시키는 작업을 하는데, 이때 여러 스레드가 동시에 increaseCount 메소드에 접근하면 동시성 문제가 발생할 수 있습니다.
그래서 increaseCount 메소드 내에서 synchronized 블록을 사용하여 동시에 한 스레드만이 해당 블록의 코드를 실행하도록 보장하고 있습니다. 여기서는 lock이라는 Object를 락 객체로 사용하여 동기화를 제공하고 있습니다.
이렇게 synchronized 키워드나 명시적인 락을 사용하면 Runnable 인터페이스를 사용하여 구현한 스레드에서도 동시성 문제를 효과적으로 해결할 수 있습니다.