[Java] Thread (1)

주영·2026년 9월 29일

멀티태스킹 (multi-tasking)

  • 하나의 CPU가 여러가지 작업을 번갈아 처리하는 방식
    → 동시에 처리하는 것처럼 보이나 사실은 CPU가 빠르게 작업을 전환하면서 동시에 처리하는 효과

멀티프로세싱 (multi-processing)

  • 여러 CPU 또는 CPU 코어를 이용해 여러 프로세스를 동시에 실행
  • 단위: 프로세스 - OS에서 관리하는 실행 단위
  • 프로세스 간 메모리를 공유하지 않으며 안정성이 높음

멀티스레딩 (multi-threading)

  • 하나의 프로세스 내에서 여러 작업들(미니 프로세스들)을 동시에 수행
  • 단위: 스레드 - 프로세스 내에서 메모리를 공유함
  • 프로세스보다 생성/소멸 비용이 낮음 → 가벼움
  • 각 스레드는 메모리를 공유하여 독립적으로 동작하며 빠른 응답에 최적
  • 동시에 접근하면 동기화 문제가 발생할 수 있음

머릿속에서 구조화가 잘 안 돼서... 클로드 도움을 받아 그림을 그려봤다...

하드웨어
⇒ CPU는 칩 1개지만, 내부에 여러 개의 코어를 가질 수가 있다
⇒ 코어는 스레드를 실제로 실행하는 장치이다
소프트웨어
⇒ OS 스케줄러가 코어에 스레드를 배정한다
⇒ 프로세스는 실행 중인 프로그램을 말하는 것으로, 독립된 메모리 공간을 가진다
⇒ 스레드가 프로세스 내부에서 돌아가는 실행 흐름이다
✲ 정리하자면... 멀티태스킹은 하나의 코어가 OS 스케줄러가 배정하는대로 여러 스레드를 번갈아 실행하는 것, 멀티프로세싱은 여러 개의 코어가 각자 여러 프로세스를 동시에 처리하는 것, 멀티스레딩은 그냥 프로세스 안에 여러 스레드가 있어서 동시에 실행할 수 있는 것...?

Java의 멀티스레드

자바는 기본 멀티스레드를 지원한다.
최소 하나의 스레드, 즉 메인 스레드를 가지고 시작하며
필요에 따라 추가적인 스레드를 생성하여 동시에 여러 작업을 처리한다.

  • 싱글스레드 애플리케이션 : 메인 스레드가 종료하면 프로세스도 종료
  • 멀티스레드 애플리케이션 : 실행 중인 스레드가 하나라도 있다면 프로세스 종료 X → Java는 이렇게 실행됨!

모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
main() 메소드의 첫 코드부터 아래로 순차적으로 실행된다.
실행 종료 조건 : 마지막 코드 실행할 때 or return문 만날 때
메인 스레드는 작업 스레드들을 만들어 병렬로 코드들을 실행한다.
→ 멀티스레드를 생성하여 멀티태스킹을 수행!
프로세스 종료 : 싱글 - 메인 스레드 종료 시 / 멀티 스레드 - 모든 스레드 종료 시

동시성 Concurrency

하나의 CPU가 작업을 쪼개 번갈아가며 실행하여 마치 동시에 실행되는 것처럼 보이는 구조
한 개의 코어로 스레드마다 돌아가면서 조금씩 실행하지만, 너무 빨라서 사람의 눈에는 독립적으로 돌아가는 것처럼 보인다.

병렬성 Parallelism

여러 CPU가 실제로 동시에 각각의 작업을 처리하는 구조
코어의 수보다 스레드 수가 작으면 각각의 코어로 병렬성이 보장되지만,
스레드의 개수가 코어보다 많은 경우 OS가 스케줄링을 통해 여떤 순서로 실행할 것인지를 결정한다.

Lab #1 Thread 생성 & 실행


public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		MyThread t1 = new MyThread();
		t1.start();
		
		Thread t2 = new Thread(new MyRunnable());
		t2.start();
		
		Thread t3 = new Thread(
				
				new Runnable() {
					
					public void run() {
						for (int i = 0; i <100; i++) {
							System.err.println("익명클래스로 만든 " + (i));
							try {
								Thread.sleep(500);
							} catch (InterruptedException e) {
								// TODO Auto-generated catch block
								e.printStackTrace();
							}
						}
					}
			
		});
		
		t3.start();
		
		Thread t4 = new Thread(()->{
			
				for (int i = 0; i <100; i++) {
					System.err.println("람다식으로 만든 " + (i));
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
		});
		
		t4.start();

	}

}

→ t1 : Thread 클래스 / t2 : Runnable 인터페이스 / t3: 익명 클래스 / t4 : 람다식

⇒ 신기했던 점은 이런 식으로 어떤 스레드가 먼저 나올지 모른다는 것... 왜냐면 내 코드가 결정하는 부분이 아니라 OS 스케줄러가 결정하는 것이기 때문이다.
⇒ run()이 아니라 start()를 호출해야 한다! - run()은 스레드가 할 일을 적어둔 것일 뿐, start()로 시작된 스레드가 run()을 실행한다.

Lab #2 반복문 / sleep 걸고 멀티스레드로 동작시키기

TestThread1,2 모두 Runnable을 구현 → Thread에 전달 → start()

⇒ 반복문을 통해 각 스레드를 10번씩 출력하도록 하고 있다.

⇒ sleep()을 통해 출력을 대기 상태에 두었다가 출력할 수 있다.
⇒ Thread.sleep을 사용하려면 try-catch문을 사용하여 InterrupedException을 써야 한다.

스레드에서 사용하는 주요 메소드

start() : 스레드를 시작한다. 이 메소드는 run() 메소드를 호출하여 스레드에서 코드를 실행한다.
run() : 스레드에서 실행될 코드를 작성하는 메소드이닫. Thread 클래스에서 기본적으로 정의 되어 있지만, Runnable 인터페이스를 구현하여 재정의해서 사용한다.
sleep(millisec) : 현재 실행 중인 스레드를 지정된 시간동안 일시정지한다.
interrupt() : 현재 Thread에 신호를 보내 중단하도록 알림을 보낸다.
join()/join(millisec) : 다른 Thread가 종료될 때까지 현재 Thread를 대기시킨다. 시간을 지정할 수도 있다.

✲ 우선순위에 대하여,
setPriority() 를 사용할 수 있으나, 기대하는 대로 항상 반영되지 않는다.
JVM은 OS의 스케줄러에 의존한다. 따라서 우선순위가 전달은 되지만 무시하거나 약하게 반영한다. 우선순위가 최대치일지라도 다른 스레드보다 먼저 실행된다는 보장은 없다.

Lab #3 짱구맹구흰둥이

public class Player1 extends Thread {
	String sound;
	int time;
	
	public Player1(String name, String sound, int time) {
		this.setName(name);
		this.sound = sound;
		this.time = time;
	}
	
	public void run() {
		int i = 0; 
		while (true) {
			if (i == time) break; 
			System.out.printf("%d %s %s \n", (i+1), this.getName(), sound);
			i++;
		}
		
		System.out.println(this.getName() + "끝");
	}
}

⇒ Player1 : Thread 클래스를 직접 상속받는 방식
⇒ Thread 클래스에 이미 name이 구현되어 있기 때문에 자식 클래스에서 별도로 다시 선언할 필요가 없다.

package Lab3;

public class Player2 implements Runnable {
	
	String name; 
	String sound;
	int time;
	
	public Player2(String name, String sound, int time) {
		this.name = name;
		this.sound = sound;
		this.time = time;
	}
	
	public void run() {
		int i = 0; 
		while (true) {
			if (i == time) break; 
			System.out.printf("%d %s %s \n", (i+1), name, sound);
			i++;
		}
		
		System.out.println(name + "끝");
	}
}

⇒ Player2 : Runnable 인터페이스 구현
⇒ Runnable에는 변수가 없으므로 변수를 선언해야 한다.

public class Main {

	public static void main(String[] args) {
		// Thread 상속 
		Thread p1 = new Player1("짱구", "신난다신난다~", 20);
		Thread p2 = new Player1("맹구", "훌쩍~", 20);
		Thread p3 = new Player1("흰둥이", "멍멍멍멍~", 20);
		
		// Runnable 구현 
		Thread th1 = new Thread(new Player2("**짱구", "신난다신난다~", 10));
		Thread th2 = new Thread(new Player2("**맹구", "훌쩍~", 10));
		Thread th3 = new Thread(new Player2("**흰둥이", "멍멍멍멍~", 10));
		
		System.out.println(p1.getName());
		System.out.println(p2.getName());
		System.out.println(p3.getName());
		
		System.out.println(th1.getName());
		System.out.println(th2.getName());
		System.out.println(th3.getName());
		
		th1.setName("짱구바보");
		System.out.println(th1.getName());
		
		p1.start();
		p2.start();
		p3.start();
		
		th1.start();
		th2.start();
		th3.start();
	}

}

⇒ Thread를 상속받은 Player1 객체를 만들 때는 Player1 자체를 Thread 타입으로 바로 받을 수가 있다!
⇒ 하지만, Runnable로 구현한 Player2 객체는 Thread로 받아주어야 한다.
⇒ 그럼 굳이 왜 Runnable을 사용하는 건지...? - 다른 클래스를 상속받을 수 있고, 작업과 스레드를 분리하여 재사용성이 증가되기 때문이다.

0개의 댓글