[김영한의 실전 자바 - 고급 1편] 스레드 생성과 실행

Turtle·2024년 8월 26일
0
post-thumbnail

🏷️JVM(Java Virtual Machine)

  • 메서드 영역(Method Area) : 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.

    • 클래스 정보 : 클래스 실행 코드, 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재한다.
    • static 영역 : static 변수들을 보관한다.
    • 런타임 상수 풀 : 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.
  • 스택 영역(Stack Area) : 자바 실행 시 하나의 실행 스택이 생성된다. 스택 영역에 쌓이는 프레임을 스택 프레임이라고 한다. 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 저장한다. 메서드가 종료되면 해당 스택 프레임이 제거된다.

  • 힙 영역(Heap Area) : 객체와 배열이 생성되는 영역이다. GC가 동작하는 주요 영역으로 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.

🏷️스레드 생성

  1. Thread 클래스를 상속
  2. Runnable 인터페이스를 구현
public class HelloThread extends Thread {

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ": run()");
	}

	public static void main(String[] args) {
		HelloThread helloThread = new HelloThread();
		// helloThread.run();	(X)
		helloThread.start();
	}
}

👉run() 메서드가 아니라 반드시 start() 메서드를 호출해야 별도의 스레드에서 run() 코드가 실행된다.

스레드 생성 전 상황

현재 main() 메서드가 실행되었고 그 결과 main이라는 이름의 스레드가 실행되는 것을 확인할 수 있다. 프로세스가 작동하려면 최소한 하나의 스레드는 있어야 한다.

스레드 생성 후 상황

스레드 객체를 생성 후 start() 메서드를 호출하면 해당 스레드를 위한 별도의 스택 공간을 할당한다. 스레드 객체를 생성하고 반드시 start()를 호출해야 스택 공간을 할당 받고 스레드가 작동한다. Thread를 상속 받아 run() 메서드를 재정의해두었는데 그 run() 메서드가 실행되어 run이라는 이름의 스택 프레임이 생성되는 것이다.

👉스레드는 실행 순서를 보장하지 않는다.

🏷️start() vs run()

쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다. 또한, 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 만약 쓰레드의 작업을 다시 한 번 더 수행해야 한다면 새로 쓰레드를 생성한 다음에 start()를 호출해야 한다.

  1. run() 메서드를 사용한 경우
public class RunThread {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + ": main() start");

		HelloThread thread = new HelloThread();
		thread.setName("내가 만든 쓰레드");
		System.out.println(Thread.currentThread().getName() + ": run() 호출 전");
		thread.run();
		System.out.println(Thread.currentThread().getName() + ": run() 호출 후");
		System.out.println(Thread.currentThread().getName() + ": main() end");
	}
}

실행 결과

main: main() start
main: run() 호출 전
main: run()
main: run() 호출 후
main: main() end

run() 메서드를 사용하게 되면 별도의 쓰레드 객체에서 run()을 실행하는 것이 아니라 main 쓰레드가 run() 메서드를 호출한 것을 볼 수 있다. 실행 그림은 아래와 같다.

  1. start() 메서드를 사용한 경우
public class StartThread {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + ": main() start");

		HelloThread thread = new HelloThread();
		thread.setName("내가 만든 쓰레드");
		System.out.println(Thread.currentThread().getName() + ": run() 호출 전");
		thread.start();
		System.out.println(Thread.currentThread().getName() + ": run() 호출 후");
		System.out.println(Thread.currentThread().getName() + ": main() end");
	}
}

실행 결과

main: main() start
main: run() 호출 전
main: run() 호출 후
main: main() end
내가 만든 쓰레드: run()

쓰레드의 start() 메서드는 쓰레드에 스택 공간을 할당하면서 쓰레드를 시작하는 아주 특별한 메서드이다. 이 해당 쓰레드에서 run() 메서드를 실행한다. main 쓰레드와 별개로 내가 만든 쓰레드라는 쓰레드가 생기고 그 쓰레드 안에 run() 스택 프레임이 쌓이게 되는 것이다. 따라서 main 쓰레드가 아닌 별도의 쓰레드에서 재정의한 run() 메서드를 실행하려면 반드시 start() 메서드를 호출해야 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출 스택을 필요로 하기 때문에 새로운 쓰레드를 생성하고 실행할 때마다 새로운 호출 스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출 스택은 소멸된다.

🏷️데몬 스레드

쓰레드는 사용자 쓰레드와 데몬 쓰레드로 구분할 수 있다.

  1. 사용자 쓰레드
  • 프로그램의 주요 작업을 수행한다.
  • 작업이 완료될 때까지 실행된다.
  • 모든 사용자 쓰레드가 종료되면 JVM도 종료된다.
  1. 데몬 쓰레드
  • 백그라운드에서 보조적인 작업을 수행한다.
  • 모든 사용자 쓰레드가 종료되면 데몬 쓰레드는 자동으로 종료된다.
public class HelloDaemonThread {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + ": main() start");
		DaemonThread daemonThread = new DaemonThread();
		daemonThread.setDaemon(true);	// 데몬 쓰레드로 설정
		daemonThread.start();
		System.out.println(Thread.currentThread().getName() + ": main() end");
	}

	static class DaemonThread extends Thread {

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + ": run() start");
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
			System.out.println(Thread.currentThread().getName() + ": run() end");
		}
	}
}

실행 결과

main: main() start
main: main() end
Thread-0: run() start

데몬 쓰레드의 run() end 부분이 출력되지 않았다. 데몬 쓰레드에서 start()를 호출했으나 main 쓰레드가 종료되면서 데몬 쓰레드 역시 자동으로 종료되기 때문이다.

또한 run() 메서드는 체크 예외를 밖으로 던질 수 없다. run() 메서드 안에서 sleep()를 호출할 때 체크 예외를 반드시 잡고 넘어가야 한다.

🏷️Runnable

public class HelloRunnable implements Runnable  {

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ": run()");
	}

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + ": main() start");

		HelloRunnable helloRunnable = new HelloRunnable();
		Thread thread = new Thread(helloRunnable);
		thread.start();

		System.out.println(Thread.currentThread().getName() + ": main() end");
	}
}

스레드 객체를 생성할 때, 실행할 작업을 생성자의 파라미터로 전달하면 된다.

👉스레드를 사용하는 두 가지 방법으로 Thread를 상속 받거나 혹은 Runnable 인터페이스를 구현하는 방법이 존재한다.

두 방식은 각각의 장단점이 있으나 Runnable 인터페이스를 구현하는 방식을 권장한다.

Thread 클래스 상속 방법

장점 : Thread 클래스를 상속 받아 run() 메서드만 재정의하면 된다.
단점 : 다이아몬드 문제 방지를 위해 자바는 다중 상속을 허용하지 않는다. 이미 다른 클래스를 상속받고 있는 경우라면 Thread 클래스를 상속받을 수 없다. 또한 인터페이스를 사용하는 방법에 비해 유연성이 떨어진다.

Runnable 인터페이스 구현 방법

장점 : 클래스의 다중 상속는 안되는 반면에 인터페이스의 경우 다중 상속이 가능하기에 문제없이 구현이 가능하다. 여러 쓰레드가 동일한 Runnable 객체를 공유할 수 있어 효율적 자원 관리가 가능하다.
단점 : 코드가 약간 복잡해질 수 있다. Runnable 객체를 생성하고 이를 Thread 생성 시 생성자 파라미터에 전달하는 과정이 추가된다.

🏷️로거(Logger) 만들기

어떤 쓰레드가 실행되는지를 명확히 확인할 수 있는 별도의 로거를 만들기

public class LoggerThread {

	private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

	public static void log(Object object) {
		String time = LocalTime.now().format(formatter);
		System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), object);
	}

	public static void main(String[] args) {
		log("String");
		log(1);
		log(true);
	}
}

🏷️여러 쓰레드 만들기

단순 for문으로 가능

import static thread.util.Logger.log;

public class ManyThread {

	public static void main(String[] args) {
		log("main() start");

		HelloRunnable helloRunnable = new HelloRunnable();
		for (int i = 0; i < 100; i++) {
			Thread thread = new Thread(helloRunnable);
			thread.start();
		}

		log("main() end");
	}
}

👉쓰레드 간의 실행 순서는 보장되지 않는다.

🏷️Runnable을 만드는 다양한 방법

정적 중첩 클래스를 사용하여 Runnable을 만들어보기

import static thread.util.Logger.log;

public class InnerRunnable {

	public static void main(String[] args) {
		log("main() start");

		Runnable runnable = new MyRunnable();
		Thread thread = new Thread(runnable);
		thread.start();

		log("main() end");
	}

	// 정적 중첩 클래스를 왜 사용했는가? → 작성할 클래스의 숫자를 줄이기 위함
	static class MyRunnable implements Runnable {

		@Override
		public void run() {
			log("run()");
		}
	}
}

인터페이스를 직접 구현하는 것처럼 보이는 익명 클래스로 Runnable을 만들어보기 + 람다식으로도 가능

public class AnonymousRunnable {

	public static void main(String[] args) {
		log("main() start");

		// 인터페이스를 직접 구현하는 익명 클래스
		Runnable runnable = new Runnable() {

			@Override
			public void run() {
				log("run()");
			}
		};

		Thread thread = new Thread(runnable);
		thread.start();
		log("main() end");
	}
}

🏷️문제와 풀이

  1. Thread 클래스를 상속받은 CounterThread라는 스레드 클래스를 만들기
  2. 이 스레드는 1 ~ 5까지의 숫자를 1초 간격으로 출력해야 한다.
  3. main() 메서드에서 CounterThread 스레드 클래스를 만들고 실행한다.
import static thread.ex1.CustomLogger.log;

public class StartTest1Main {

	public static void main(String[] args) {
		CounterThread counterThread = new CounterThread();
		counterThread.start();
	}

	static class CounterThread extends Thread {

		@Override
		public void run() {
			for (int i = 1; i <= 5; i++) {
				log("value: " + i);
				try {
					Thread.sleep(1000);	// 1초 간격
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
		}
	}
}
  1. CounterRunnable 라는 이름의 클래스를 만드는데 이 클래스는 Runnable 인터페이스를 구현해야 한다.
  2. CounterRunnable은 1 ~ 5까지의 숫자를 1초 간격으로 출력해야 한다.
  3. main() 메서드에서 CounterRunnable 인스턴스를 이용해서 Thread를 생성하고 실행한다.
import static thread.ex1.CustomLogger.log;

public class StartTest2Main {

	public static void main(String[] args) {
		Thread thread = new Thread(new CounterRunnable(), "counter");
		thread.start();
	}

	static class CounterRunnable implements Runnable {

		@Override
		public void run() {
			for (int i = 1; i <= 5; i++) {
				log("value: " + i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

위의 코드를 익명 클래스로 구현해본다.

import static thread.ex1.CustomLogger.log;

public class StartTest3Main {

	public static void main(String[] args) {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				for (int i = 1; i <= 5; i++) {
					log("value: " + i);
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};

		Thread thread = new Thread(runnable, "counter");
		thread.start();
	}
}

여러 스레드를 생성하는 코드를 작성한다.

import static thread.ex1.CustomLogger.log;

public class StartTest4Main {

	public static void main(String[] args) {

		Thread threadA = new Thread(new CustomThread("Thread-A", 1000));
		Thread threadB = new Thread(new CustomThread("Thread-B", 500));

		threadA.start();
		threadB.start();
	}

	static class CustomThread implements Runnable {

		private final String content;
		private final int sleepMs;

		public CustomThread(String content, int sleepMs) {
			this.content = content;
			this.sleepMs = sleepMs;
		}

		@Override
		public void run() {
			while (true) {
				log(content);
				try {
					Thread.sleep(sleepMs);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

0개의 댓글