자바 Thread 선언방법 2가지와 각 방법별 어떤 장점있고 어떤 차이점이 있고 어떻게 동작하는지 예시코드를 통해 알아보겠습니다.
자바 Thread 선언 방법 2가지 각 특징 비교 및 동작순서
프로세스와 쓰레드
- 프로세스 : 실행 중인 프로그램이며, OS로부터 실행에 필요한 메모리를 할당받게 된다.
- 쓰레드 : 프로세스의 자원을 사용해 실제 작업을 수행한다. 모든 프로세스는 최소 하나의 쓰레드가 존재한다.
쓰레드는 프로세스라는 공장에서 일하는 일꾼으로 비유할 수 있다. 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라고 부르는데 이 경우에는 공장 하나에 여러 일꾼이 일하는 모습로 생각할 수 있다.
멀티태스킹과 멀티쓰레딩
- 멀티태스킹 : 하나의 CPU에서 여러 개의 프로세스를 번갈아가며 실행하여 동시에 처리하는 것.
- 멀티스레딩 : 하나의 프로세스 내에서 스레드 간에 작업을 나눠서 처리하는 것.
멀티스레딩은 여러 스레드간에 작업 전환(context switching)이 일어나기 때문에, 싱글쓰레드보다 작업시간은 길어질 수 있다.
멀티스레딩 문제점
멀티스레딩은 자원공유를 통해 성능을 높일 수 있지만, 동기화, 교착상태의 문제가 발생할 수 있다. 여러 스레드가 공유하는 자원에 동시에 접근하여 처리하면 데이터 일관성이 깨질 수 있기 때문. 이때는 동기화(synchronization)나 스케줄링을 통해 문제를 해결할 수 있다.
쓰레드를 구현하는 방법 2가지와 예시코드
1. Thread 클래스 상속받기
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread is running!");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
Thread를 상속받아 구현한 후 인스턴스를 생성하면 오버라이딩한 run 메서드를 수행하는 스레드가 생성된다. 자바는 클래스를 하나만 상속받을 수 있기 때문에(다중상속 금지) MyThread 클래스는 다른 클래스를 상속받을 수 없다는 제약이 생긴다. 대신 부모 클래스인 Thread에서 구현된 메서드들을 바로 호출해 사용할 수 있다.
2. Runnable 인터페이스 구현하기
public class MyRunnable implements Runnable {
@Override
public void run() {
// 스레드에서 실행할 코드 작성
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
thread1.start();
thread2.start();
}
}
MyRunnable 클래스는 Runnable 인터페이스를 상속받아 run 메서드를 오버라이딩한다. 이때 Thread 클래스를 상속하는 방식과 다른 부분은 MyRunnable 클래스의 인스턴스 자체가 Thread가 아니며, 작업에 대한 정의라는 점이다. Runnable 인스턴스는 Thread의 생성자에 넘겨 Thread가 만들어진다.
이러한 부분때문에 Runnable을 구현하는 방식으로 Thread를 생성하게 되면 자연스럽게 작업과 작업을 수행하는 대상이 분리가 된다. Thread는 Runnable 구현 클래스의 작업내용을 전달받아 실행하는 역할만 담당하게 되는 것이다.
Runnable 구현 클래스로 Thread 만들때의 특징
1. Runnable은 작업과 실행대상이 나누어져있음
Thread를 상속받는 방식과는 다르게 Runnable 구현하는 클래스는 작업내용만을 정의한다. 해당 클래스에는 run 하나만 정의되어 있다. 이 경우에는 Thread 클래스가 Runnable 구현 클래스 인스턴스를 관리하는 역할을 맡게 된다.
2. 상대적으로 객체지향적인 코드 작성가능하고 그 장점을 누릴 수 있음
- 재사용성 : Runnable을 사용하면 작업과 관리/실행 주체가 분리되었기 때문에, 다른 클래스에서도 정의한 작업을 실행할 수 있기 때문에 재사용성이 높다.
- 일관성 : 동일한 인터페이스를 사용해 구현하기 때문에, Thread가 실행할 서로 다른 작업내용을 정의할 때 동일하게 run 메서드를 통해 구현하게 된다. 이 점은 인터페이스를 사용해 생긴 장점이다.
- 유연성 : Runnable을 구현한 클래스에서 다른 인터페이스도 구현할 수 있기 때문에, 동일한 객체를 여러가지 방법으로 실행할 수 있음.
- 유지보수와 가독성 : Runnable은 자바에서 제공하는 표준 인터페이스이기 때문에 다른 개발자가 코드를 봐도 쉽게 이해하고 작성할 수 있다는 장점이 있다. 또한 작업에 대한 정보를 보려면 run 메서드만 확인하면 되기 때문에 캡슐화하여 작업 내용을 더 쉽게 이해하고 관리할 수 있다.
3. Functional Interface
Runnable r = () -> System.out.println("Hello, World!");
Thread t = new Thread(r);
t.start();
Runnable 인터페이스는 run 추상메서드 1개를 가지고 있는 Functional 인터페이스이기 때문에 이렇게도 Thread를 생성할 수 있다. 실행할 작업을 파라미터로 넘겨받을 수 있다.
4. 참고) Callable
Thread를 생성할때는 Runnable 또는 Callable 둘다 가능한데, 두 인터페이스의 차이점은 Runnable은 반환값이 없고(void 타입) Callable은 반환값이 존재한다.
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 1 + 2;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
Future<Integer> f = Executors.newSingleThreadExecutor().submit(c);
int result = f.get();
System.out.println(result); // 3
}
}
Thread가 Runnable 구현 클래스의 작업내용을 실행할때 내부 동작순서
Thread 클래스는 아래와 같이 구현되어있다. 아래 코드는 Runnable 객체로 작업을 수행하는데 필요한 내용을 제외한 코드이다.
public class Thread implements Runnable {
private Runnable target; // Runnable 객체를 참조할 필드
public Thread() {
target = null;
}
public Thread(Runnable target) {
this.target = target;
}
public void run() {
if (target != null) {
target.run(); // target이 참조하는 Runnable 객체의 run() 메서드를 실행
}
}
public void start() {
// 쓰레드 실행
}
// 기타 필드와 메서드 생략
}
Runnable 객체는 생성자를 통해 필드에 저장되면 Thread 인스턴스 저장해둔다. 이후에 Thread에 start 함수를 호출하면 참조하고 있는 Runnable 인스턴스의 run을 호출하여 작업가능 여부를 확인하고 작업을 수행한다.
document에서는 start 메서드를 실행하면 JVM이 현재 Thread의 run을 실행시킨다고 명시되어있다. Thread 클래스 내부코드를 보고 대략 예상되는 호출 흐름을 다이어그램으로 그려보았다. (실제 동작과는 차이가 있으므로 참고만 해주세요)
Main Thread에서 MyRunnable 클래스 인스턴스를 이용해 Thread를 생성하고 Thread를 실행했을 때의 sequence 다이어그램이다. MyRunnable 인스턴스는 Thread 인스턴스의 필드에 참조변수에 저장되고 start가 호출되면 내부적으로 native method가 호출되어 OS의 Thread 할당되고 run이 호출되며 작업이 실행된다.
두 방식에서 현재 실행 중인 Thread 정보를 가져오는 방법 차이점
부모 클래스로 직접 접근 vs Thread 클래스 static 메서드로 참조변수 가져오기
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " is running");
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("Thread 1");
t2.setName("Thread 2");
t1.start();
t2.start();
}
}
Thread 구현하는 방식은 위와 같이 부모 클래스의 getName() 메서드를 호출해서 현재 실행 중인 스레드의 정보를 직접 가져올 수 있다. 위 경우에는 getName() 메서드 앞에 super가 생략되어 있다. 부모 클래스인 Thread의 getName()을 호출한다.
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.setName("Thread 1");
t2.setName("Thread 2");
t1.start();
t2.start();
}
}
MyRunnable 클래스의 인스턴스가 Thread가 아니기 때문에, Thread 정보를 가져오기 위해서 Thread 클래스의 static 메서드를 통해 현재 실행 중인 스레드의 참조변수를 가져와 이름을 가져올 수 있다.
To-do
- 스레드가 한번 사용하고 다시 사용하면 발생하는 오류.
- Runnable로 구현된 Thread가 실행했을 때 일어나는 일과 native method (내부 동작원리)에 대해 알아볼 것.
- JVM과 ThreadGroup 관계에 대해서 알아볼 것.
- Tomcat에서 Thread가 실행될 때 동작순서.
- 객체지향적인 장점의 예시코드를 추가한 포스팅을 할 것.
- 새로운 Thread가 생성될때, 이전에 종료된 Thread와 같은 이름을 가진 Thread가 생성될 수 도 있는지 여부 확인하기.
참고문서
- 자바의 정석 13장 Thread
- https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#start--
'Skills (스터디) > Java' 카테고리의 다른 글
[Java] 특정 클래스에 선언된 필드명, 자료형 가져오기 - Reflection (1) | 2024.09.15 |
---|---|
[Java] Reflection 기본개념 및 동작원리 (0) | 2024.09.15 |
[Java] parallelStream() 메서드 사용시 예상되는 문제점 및 해결방안 [2] (0) | 2023.02.26 |
[Java] parallelStream() 메서드 사용시 예상되는 문제점 및 해결방안 [1] (0) | 2023.02.26 |
[Maven] 간단한 Multi-Module 프로젝트 만들기 (0) | 2023.02.11 |
최근댓글