Notice
Recent Posts
Recent Comments
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

SYDev

[이것이 자바다] Chapter 14. 멀티 스레드 본문

Programming Lang/Java

[이것이 자바다] Chapter 14. 멀티 스레드

시데브 2025. 2. 1. 20:07
  • 데이터 분할 병렬 처리할 때
  • 안드로이드 앱에서 네트워크 통신할 때
  • 다수의 클라이언트 요청을 처리하는 서버를 개발할 때

1. 메인 스레드

  • 모든 자바 프로그램은 main thread가 main() method를 실행하면서 시작
  • main thread는 main() method의 첫 코드부터 순차적으로 실행
  • main() method의 마지막 코드를 실행하거나, return 문을 만나면 실행 종료

  • main thread는 필요에 따라 추가 작업 스레드들을 만들어 실행

  • 실행중인 thread가 하나라도 있다면 process는 종료되지 않음

2. 작업 스레드 생성과 실행

2.1. Thread 클래스로 직접 생성

Thread thread = new Thread(Runnable target);
  • Runnable: threaed가 task를 실행할 때 사용하는 interface
    • run() method가 정의돼있음 -> 구현 클래스는 thread가 실행할 코드를 포함
class Task implements Runnable {
    @Override
    public void run() {
        // thread가 실행할 코드
    }
}
  • 명시적인 Runnable 구현 클래스를 작성하지 않고, Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용하는 방법을 더 많이 사용
Thread thread = new Thread( new Runnable() {
    @Override
    public void run() {
        // thread가 실행할 코드
    }
} );
  • 작업 스레드 객체를 생성하고, 작업 스레드를 실행하려면 thread 객체의 start() method를 호출
thread.start();

- 예제

package ch14.sec3;

import java.awt.Toolkit;

public class BeepPrintExample {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i = 0; i < 5; i++) {
			toolkit.beep();
			try { Thread.sleep(500); } catch(Exception e) {}
		}
		
		for(int i = 0; i < 5; i++) {
			System.out.println("띵");
			try { Thread.sleep(500); } catch(Exception e) {}
		}
	}
}

-> beep음을 모두 발생시키고 나서, "띵" 5번 출력

 

package ch14.sec3;

import java.awt.Toolkit;

public class BeepPrintExample {
	public static void main(String[] args) {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				Toolkit toolkit = Toolkit.getDefaultToolkit();
				for(int i = 0; i < 5; i++) {
					toolkit.beep();
					try { Thread.sleep(500); } catch(Exception e) {}
				}
			}
		});
		
		// 작업 thread 실행
		thread.start();
		
		for(int i = 0; i < 5; i++) {
			System.out.println("띵");
			try { Thread.sleep(500); } catch(Exception e) {}
		}
	}
}

-> main thread는 출력을 담당, 작업 thread에서는 동시에 beep음 처리

 

2.2. Thread 자식 클래스로 생성

  • 작업 thread 객체를 생성하는 또 다른 방법 -> Thread의 자식 객체로 만드는 방법
  • Thread 클래스를 상속한 다음 run() method를 재정의 -> thread가 실행할 코드를 작성하고 객체 생성
public class WorkerThread extends Thread {
    @Override 
    public void run() {
        // thread가 실행할 코드
    }
}

// thread 객체 생성
Thread thread = new WorkerThread();

  • Thread 익명 자식 객체를 사용하는 방법
Thread thread = new Threaed() {
    @Override
    public void run() {
        // thread가 실행할 코드
    }
};

thread.start();

- 예제

package ch14.sec3_1;

import java.awt.Toolkit;

public class BeepPrintExample {
	public static void main(String[] args) {
		Thread thread = new Thread() {
			@Override
			public void run() {
				Toolkit toolkit = Toolkit.getDefaultToolkit();
				for(int i = 0; i < 5; i++) {
					toolkit.beep();
					try { Thread.sleep(500); } catch(Exception e) {}
				}
			}
		};
		
		thread.start();
		
		for(int i = 0; i < 5; i++) {
			System.out.println("띵");
			try { Thread.sleep(500); } catch(Exception e) {}
		}
	}
}

 

3. 스레드 이름

  • thread는 자신의 이름을 가짐
    • main thread는 'main'이라는 이름을 가짐
    • worker thread는 자동적으로 'Thread-n'이라는 이름을 가짐
  • worker thread 이름을 다른 이름으로 설정하고 싶다면, Thread 클래스의 setName() method 사용
thread.setName("스레드 이름");
  • thread name은 주로 디버깅할 때, 어떤 thread가 작업을 하는지 조사할 목적으로 주로 사용
  • 현재 코드를 어떤 thread가 실행하고 있는지 확인하려면 -> static method인 currentThread()로 thread 객체의 참조를 얻은 다음, getName() method name을 출력
Thread thread = Thread.currentThreaed();
System.out.println(threaed.getName());

 

4. 스레드 상태

  • thread object를 생성(NEW)하고, start() method를 호출하면 바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RUNNABLE)가 됨
  • 실행(RUNNING) 상태: CPU scheduling에 따라 CPU를 점유하고 run() method를 실행
    • running thread는 run() method를 모두 실행하기 전에 scheduling에 의해, 다시 실행 대기 상태로 돌아갈 수 있음
  • 실행 상태에서 run() method 종료 -> 더이상 실행할 코드가 없기 때문에 threaed의 실행이 멈춤 -> 종료(TERMINATED) 상태
  • 실행 상태에서 일시 정지 상태로 가기도 함 -> 일시 정지 상태는 thread가 실행할 수 없는 상태를 의미
    • 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야함

4.1. 주어진 시간 동안 일시 정지

try {
    Thread.sleep(1000);
} catch(InterruptedException e) {
    // interrupt() method가 호출되면 실행
}

- 예제

package ch14.sec5.exam1;

import java.awt.Toolkit;

public class SleepExample {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i = 0; i < 10; i++) {
			toolkit.beep();
			try {
				Thread.sleep(3000);
			} catch(InterruptedException e) {
			}
		}
	}
}

-> 3초 주기로 beep음을 10번 발생

 

4.2. 다른 스레드의 종료를 기다림

  • 다른 thread가 종료될 때까지 기다렸다가 실행해야 하는 경우

- 예제

package ch14.sec5.exam2;

public class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}
	
	public void setSum(long sum) {
		this.sum = sum;
	}
	
	@Override
	public void run() {
		for(int i = 1; i <= 100; i++) {
			sum += i;
		}
	}
}
package ch14.sec5.exam2;

public class JoinExample {
	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
		try {
			sumThread.join();	//sumThread 대기
		} catch (InterruptedException e) {
		}
		System.out.println("1~100 합: " + sumThread.getSum());
	}
}

  • join으로 기다리지 않는 경우, 그냥 끝내버림
package ch14.sec5.exam2;

public class JoinExample {
	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
//		try {
//			sumThread.join();	//sumThread 대기
//		} catch (InterruptedException e) {
//		}
		System.out.println("1~100 합: " + sumThread.getSum());
	}
}

 

4.3. 다른 스레드에게 실행 양보

  • thread가 처리하는 작업은 무의미안 반복문을 실행하는 경우가 존재 -> 이때 다른 thread에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움
public void run() {
    while(true) {
        if(work) {
            System.out.println("ThreadA 작업 내용");
        }
    }
}

-> 무의미한 반복

public void run() {
    while(true) {
        if(work) {
            System.out.println("ThreadA 작업 내용");
        } else {
            Thread.yield();
        }
    }
}

- 예제

package ch14.sec5.exam3;

public class WorkThread extends Thread {
	// field
	public boolean work = true;
	
	// constructor
	public WorkThread(String name) {
		setName(name);
	}
	
	// method
	@Override
	public void run() {
		while(true) {
			if(work) {
				System.out.println(getName() + ": 작업처리");
			} else {
				Thread.yield();
			}
		}
	}
}
package ch14.sec5.exam3;

public class YieldExample {
	public static void main(String[] args) {
		WorkThread workThreadA = new WorkThread("workThreadA");
		WorkThread workThreadB = new WorkThread("workThreadB");
		workThreadA.start();
		workThreadB.start();
		
		try { Thread.sleep(5000); } catch (InterruptedException e) {}
		workThreadA.work = false;
		
		try { Thread.sleep(10000); } catch (InterruptedException e) {}
		workThreadA.work = true;
	}
}

 

5. 스레드 동기화

  • 멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있음 -> 다른 thread에 의해 의도했던 것과 다른 결과 초래 가능성
  • 이를 위해 자바는 synchronized method와 block을 제공
  • 객체 내부에 synchronized method와 block이 여러 개 있으면, thread가 이들 중 하나를 실행할 때 다른 thread는 해당 method는 물론이고 다른 synchronized method 및 block실행 불가
  • 일반 method는 실행 가능

5.1. 동기화 메소드 및 블록 선언

public synchronized void method() {
    // 단 하나의 thread만 실행하는 영역
}
  • 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶은 경우
public void method() {
    // multiple threads가 실행 가능한 영역
    
    synchronized(공유객체) {
        // 단 하나의 thread만 실행 가능한 영역
    }
    
    // multiple threads가 실행 가능한 영역
}

- 예제

package ch14.sec6.exam1;

public class Calculator {
	private int memory;
	
	public int getMemory() {
		return memory;
	}
	
	// synchronized method
	public synchronized void setMemory1(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);
		} catch(InterruptedException e) {}
		System.out.println(Thread.currentThread().getName() + ": " + this.memory);
	}
	
	// synchronized block
	public void setMemory2(int memory) {
		synchronized(this) {	
			this.memory = memory;
			try {
				Thread.sleep(2000);
			} catch(InterruptedException e) {}
			System.out.println(Thread.currentThread().getName() + ": " + this.memory);
		}
	}
}

package ch14.sec6.exam1;

public class User1Thread extends Thread {
	private Calculator calculator;
	
	public User1Thread() {
		setName("User1Thread");
	}
	
	public void setCalculator(Calculator calculator) {
		this.calculator = calculator;
	}
	
	@Override
	public void run() {
		calculator.setMemory1(100);
	}
}

package ch14.sec6.exam1;

public class User2Thread extends Thread {
	private Calculator calculator;
	
	public User2Thread() {
		setName("User2Thread");
	}
	
	public void setCalculator(Calculator calculator) {
		this.calculator = calculator;
	}
	
	@Override
	public void run() {
		calculator.setMemory2(50);
	}
}
package ch14.sec6.exam1;

public class SynchronizedExample {
	public static void main(String[] args) {
		Calculator calculator = new Calculator();
		
		User1Thread user1Thread = new User1Thread();
		user1Thread.setCalculator(calculator);
		user1Thread.start();
		
		User2Thread user2Thread = new User2Thread();
		user2Thread.setCalculator(calculator);
		user2Thread.start();
	}
}

-> 동기화 블록 혹은 메소드 선언을 했기 때문에, 매개값을 입력한대로 예상한 결과 출력

-> setMemory에서 동기화 선언을 제거하고 실행한 경우

 

5.2. wait()과 notify()를 이용한 스레드 제어

  • 두 개의 threads를 교대로 번갈하가며, 교대 작업이 필요할 경우
  • 공유 객체 -> 두 threads가 작업할 내용을 각각 동기화 method로 설정
  • 한 thread가 작업을 완료하면, notify() method 호출
    • 일시 정지 상태의 thread -> 실행 대기 상태
  • 자신은 두 번 작업을 하지 않도록 wait() method 호출
    • 실행 -> 일시 정지 상태
  • wait() & notify()는 synchronized method or synchronized block 내에서만 사용 가능

- 예제

package ch14.sec6.exam2;

public class WorkObject {
	// synchronized method
	public synchronized void methodA() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodA 작업 실행");
		notify();	// 다른 thread를 실행 대기 상태로 만
		try {
			wait();	// 자신의 thread를 일시 정지 상태로 만
		} catch (InterruptedException e) {
		}
	}
	
	public synchronized void methodB() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodB 작업 실행");
		notify();
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}
}

package ch14.sec6.exam2;

public class ThreadA extends Thread {
	private WorkObject workObject;
	
	// parameter로 공유 작업 객체를 받음
	public ThreadA(WorkObject workObject) {
		setName("ThreadA");
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i = 0; i < 10; i++) {
			workObject.methodA();	// synchronized method 호
		}
	}
}

package ch14.sec6.exam2;

public class ThreadB extends Thread {
	private WorkObject workObject;
	
	// parameter로 공유 작업 객체를 받음
	public ThreadB(WorkObject workObject) {
		setName("ThreadB");
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i = 0; i < 10; i++) {
			workObject.methodB();	// synchronized method 호
		}
	}
}
package ch14.sec6.exam2;

public class WaitNotifyExample {
	public static void main(String[] args) {
		WorkObject workObject = new WorkObject();
		
		ThreadA threadA = new ThreadA(workObject);
		ThreadB threadB = new ThreadB(workObject);
		
		threadA.start();
		threadB.start();
	}
}

 

6. 스레드 안전 종료

  • thread는 run() method가 모두 실행되면 자동적으로 종료되지만, 경우에 따라 실행중인 thread를 즉시 종료할 필요가 있음
    • ex) 동영상을 끝까지 보지 않고, 사용자가 멈춤을 요구하는 경우
  • thread를 강제 종료시키기 위해 Thread는 stop() method를 제공 -> deprecated(더 이상 사용되지 않음)
    • resources(file, network, ...)가 불안정한 상태로 남겨지기 때문
  • thread를 안전하게 종료하는 방법: 사용하던 resources를 정리하고, run() method를 빨리 종료하는 것
    • 조건 이용
    • interrupt() method

6.1. 조건 이용

public class XXXThread extends Thread {
    private boolean stop;
    
    public void run() {
    	// stop이 true가 되면 반복문 탈출
        while( !stop ) {
        	// thread가 반복 실행하는 코드;
        }
    	// thread가 사용한 resource 정리
    }
}

- 예제

package ch14.sec7.exam1;

public class PrintThread extends Thread {
	private boolean stop;
	
	public void setStop(boolean stop) {
		this.stop = stop;
	}
	
	@Override
	public void run() {
		while(!stop) {
			System.out.println("실행 중");
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}
package ch14.sec7.exam1;

public class SafeStopExample {
	public static void main(String[] args) {
		PrintThread printThread = new PrintThread();
		printThread.start();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		
		printThread.setStop(true);
	}
}

-> main thread 3초 재웠다가, stop을 true로 설정

 

6.2. interrupt() 메소드 이용

  • interrupt() method: 일시 정지 상태에 있을 때, InterruptedException 예외를 발생시키는 역할
  • 예외 처리를 통해 run() method를 정상 종료 가능

- 예제

package ch14.sec7.exam2;

public class PrintThread extends Thread {
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}
		} catch(InterruptedException e) {
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}
package ch14.sec7.exam2;

public class InterruptExample {
	public static void main(String[] args) {
		Thread thread = new PrintThread();
		thread.start();
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
		}
		
		thread.interrupt();
	}
}

 

7. 데몬 스레드

  • 데몬(Daemon) 스레드: main thread의 작업을 돕는 보조적인 역할을 수행하는 thread
  • main thread가 종료되면, daemon thread도 따라서 자동으로 종료
  • ex) 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 가비지 컬렉터 -> main thread(워드프로세스, 미디어 플레이어, JVM)가 종료되면 자동 종료
  • thread를 daemon으로 만들기 위해, main thread가 daemon이 될 thread의 setDaemon(true)를 호출
public static void main(String[] args) {
    AutoSaveThread thread = new AutoSaveThread();
    thread.setDaemon(true);
    thread.start();
    ...
}

- 예제

package ch14.sec8;

public class AutoSaveThread extends Thread {
	public void save() {
		System.out.println("작업 내용을 저장함.");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				break;
			}
			save();
		}
	}
}
package ch14.sec8;

public class DaemonExample {
	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		
		System.out.println("메인 스레드 종료");
	}
}

-> main thread 종료 시에, 자동으로 종료

 

8. 스레드풀

  • 병렬 작업 처리가 많아지면 threads의 개수가 폭증 -> CPU가 바빠지고, memory 사용량이 늘어남 -> application 성능 저하
  • 병렬 작업 증가로 인한 threads 폭증 방지 -> 스레드풀(ThreadPool) 사용
  • ThreadPool: 작업 처리에 사용되는 threads를 제한된 개수만큼 정해놓고, Job Queue에 들어오는 작업들을 thread가 하나씩 맡아 처리하는 방식

8.1. 스레드풀 생성

  • 자바는 ThreadPool을 생성하고 사용할 수 있도록, java.util.concurrent 패키지에서 ExecutorService interface와 Executors class를 제공
  • 초기 수: 스레드풀이 생성될 때 기본적으로 생성되는 threads 수
  • 코어 수: threads가 증가된 후 사용되지 않는 threads를 제거할 때, 최소한 풀에서 유지하는 threads 수
  • 최대 수: 증가되는 threads의 한도 수
메소드명(매개변수) 초기 수 코어 수 최대 수
newCachedThreadPool() 0 0 Integer.MAX_VALUE
newFixedThreadPool(int nThreads) 0 생성된 수 nThreads
  • newCachedThreadPool() method
    • 초기 수와 코어 수가 0
    • 작업 개수가 많아지면 새 thread를 생성시켜 작업 처리
    • 60초 동안 thread가 아무 작업을 하지 않으면 thread를 pool에서 제거
  • newFixedThreadPool() method
    • 초기 수 0
    • 작업 개수가 많아지면 최대 nThreads개까지 thread를 생성시켜 작업을 처리
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(5);
  • method 사용 없이, 직접 ThreadPoolExecutor로 threadpool 생성도 가능
ExecutorService threadPool = new ThreadPoolExecutor(
    3, // 코어 스레드 개수
    100, // 최대 스레드 개수
    120L, // 놀고 있는 시간
    TimeUnit.SECONDS, // 놀고 있는 시간 단위
    new SynchronousQueue<Runnable>() // 작업 큐
);

-> 120초 작업하지 않을 시에, 해당 thread를 pool에서 제거

8.2. 스레드풀 종료

  • threadPool의 thread는 기본적으로 Daemon thread가 아니기 때문에, 자동 종료 X
  • ExecutorService의 다음 두 메소드 중 하나를 실행해서 종료해야 함
리턴 타입 메소드명(매개변수) 설명
void shutdown() 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있던 모든 작업을 처리한 뒤에 threadPool을 종료시킴
List<Runnable> shutdownNow() 현재 작업 처리 중인 thread를 interrupt해서 작업을 중지시키고 threadPool을 종료시킴. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록
  • shutdown(): 남아있는 작업을 마무리하고, threadPool을 종료
  • shutdownNow(): 남아있는 작업과 상관없이 강제 종료

8.3. 작업 생성과 처리 요청

  • 하나의 작업은 Runnable 또는 Callable 구현 객체로 표현됨
    • 둘의 차이점은 작업 처리 완료 후 리턴값의 유무
new Runnable() {
    @Override
    public void run() {
        // 스레드가 처리할 작업 내용
    }
}

new Callable<T>() {
    @Override
    public T call() throws Exception {
        // 스레드가 처리할 작업 내용
        return T;
    }
}
  • 작업 처리 요청: ExecutorService의 job Queue에 Runnable 또는 Callable 객체를 넣는 행위
리턴 타입 메소드명(매개변수) 설명
void execute(Runnable command) - Runnable을 job Queue에 저장
- 작업 처리 결과를 리턴 X
Future<T> submit(Callable<T> task) - Callable을 job Queue에 저장
- 작업 처리 결과를 얻을 수 있도록 Future를 리턴

- 예제

package ch14.sec9.exam2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RunnableExecuteExample {
	public static void main(String[] args) {
		// 1000개의 메일 생성
		String[][] mails = new String[1000][3];
		for(int i = 0; i < mails.length; i++) {
			mails[i][0] = "admin@my.com";
			mails[i][1] = "member" + i + "@my.com";
			mails[i][2] = "신상품 입고";
		}
		
		// ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		
		// 이메일을 보내는 작업 생성 및 처리 요청
		for(int i = 0; i < 1000; i++) {
			final int idx = i;
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					Thread thread = Thread.currentThread();
					String from = mails[idx][0];
					String to = mails[idx][1];
					String content = mails[idx][2];
					System.out.println("[" + thread.getName() + "]" + from + " ==> " + to + ": " + content);
				}
			});
		}
		
		// ExecutorService 종료
		executorService.shutdown();
		
	}
}

-> 1000개의 Runnable 생성 후, execute() method로 job Queue에 입력

-> ExecutorService는 최대 5개의 threads로 job Queue에서 Runnable을 하나씩 꺼내어 run() method를 실행하며 작업 처리


참고자료