Study/Java

[Java] 멀티 쓰레드(Multi Thread)

개발개발개발 2021. 9. 14. 20:49

1. Main Thread

모든 Java Application은 main thread 가 main 메소드를 실행하면서 시작된다. 

이러한 메인 쓰레드안에서 싱글 스레드가 아닌 멀티 스레드 애플리케이션은 필요에 따라 작업 스레드를 만들어 병렬로 코드를 실행할 수 있다. Multi Thread는 메인 스레드가 종료되어도 실행 중인 Thread가 하나라도 있으면 프로세스는 종료되지 않는다. 

 

 

2. Thread 생성 

2.1 Thread 클래스로부터 직접 생성

java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개 값으로 갖고 있는 생성자를 호출한다. Runnable에는 run() 메소드 하나가 정의되어 있으며, 구현 클래스에서 run()을 재정의하여 실행할 코드를 작성한다. 

public class Multi_thread  implements Runnable{
	@Override
	public void run() {
		// 스레드 실행 코드 
	}
}

 

2.1.1 첫 번째 방법

- Runnable 구현 객체 생성 후, 이것을 매개값으로 Thread 생성자 호출

public class Multi_thread  implements Runnable{
	@Override
	public void run() {
		int idx = 1;
		for(int i = 0; i<10; i++) {
			idx++;
			System.out.println(idx);
		}
		System.out.println(Thread.currentThread() + "최종 idx: " + idx);
	}
}
public class MultiThread {
	public static void main(String[] args) {
		Runnable multi = new Multi_thread();
		Thread subThread = new Thread(multi);
		subThread.start();
	}
}

subThread 쓰레드 실행 결과

2.1.2 두 번째 방법

- Thread 생성자 호출 시 익명 구현 객체를 생성하여 매개 값으로 사용

public class MultiThread {
	public static void main(String[] args) {
		Runnable multi = new Runnable() {
			@Override
			public void run() {
				int idx = 1;
				for(int i = 0; i<10; i++) {
					idx++;
					System.out.println(idx);
				}
				System.out.println(Thread.currentThread() + "최종 idx: " + idx);
			}
		}; //Runnable 끝
		Thread subThread = new Thread(multi);
		subThread.start();
	}
}

2.1.3 세 번째 방법

- 람다식을 매개 값으로 사용

Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스이다. 

public class MultiThread {
	public static void main(String[] args) {
		Runnable multi = () -> {
			int idx = 1;
			for(int i = 0; i<10; i++) {
				idx++;
				System.out.println(idx);
			}
			System.out.println(Thread.currentThread() + "최종 idx: " + idx);
		};
		 Thread subTread = new Thread(multi);
		  subTread.start();
	}
}
public class MultiThread {
	public static void main(String[] args) {
		Thread multi = new Thread(() -> {
			int idx = 1;
			for(int i = 0; i<10; i++) {
				idx++;
				System.out.println(idx);
			}
			System.out.println(Thread.currentThread() + "최종 idx: " + idx);
		});
		 Thread subTread = new Thread(multi);
		  subTread.start();
	}
}

 

2.2 Thread 하위 클래스로부터 생성

작업 스레드가 실행할 작업을 Runnable 로 구현하지 않고, Thread를 상속한 새로운 클래스를 정의하여 run 메소드를 overriding 하여 재정의 한다.  또는 Thread 익명 객체로 작업 스레드 객체를 생성할 수 있다. 

//Thread 클래스 상속
public class Multi_thread extends Thread {
	@Override
	public void run() {
		int idx = 1;
		for(int i = 0; i<10; i++) {
			idx++;
			System.out.println(idx);
		}
		System.out.println(Thread.currentThread() + "최종 idx: " + idx);
	}
}
public class MultiThread {
	public static void main(String[] args) {
		Thread subThread = new Thread() {
			public void run() {
				int idx = 1;
				for(int i = 0; i<10; i++) {
					idx++;
					System.out.println(idx);
				}
				System.out.println(Thread.currentThread() + "최종 idx: " + idx);
			}
		};
		subThread.start();
	}
}

 

2.3 스레드 이름 생성

기본 스레드는 thread.getName()으로 "Thread-N" 이라는 이름을 가져온다. 그리고 setName("???")을 통해 스레드의 이름을 지정할 수 있다. 

public static void main(String[] args) {
	Thread subThread = new Thread() {
		public void run() {
			int idx = 1;
			for(int i = 0; i<10; i++) {
				idx++;
				System.out.println(idx);
			}
			System.out.println(Thread.currentThread().getName() + "  최종 idx: " + idx);
		}
	};
	Thread multiThread = new Multi_thread();
	subThread.start();
	multiThread.start();
	}
}
2
3
4
5
6
7
8
9
10
11
2
3
4
Thread-0  최종 idx: 11
5
6
7
8
9
10
11
Thread-1  최종 idx: 11

setName을 통한 스레드 이름 설정 

public class MultiThread {
	public static void main(String[] args) {
		Thread subThread = new Thread() {
			public void run() {
				int idx = 1;
				for(int i = 0; i<10; i++) {
					idx++;
					System.out.println(idx);
				}
				System.out.println(Thread.currentThread().getName() + "  최종 idx: " + idx);
			}
		};
		Thread multiThread = new Multi_thread();
		subThread.setName("subThread");
		multiThread.setName("multiThread");
		subThread.start();
		multiThread.start();
	}
}
2
3
4
5
6
7
8
9
10
11
subThread  최종 idx: 11
2
3
4
5
6
7
8
9
10
11
multiThread  최종 idx: 11

3. 스레드 우선 순위 

- 동시성 : 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질

- 병렬성 : 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질

 

3.1 스레드 스케쥴링

- 우선 순위 방식 (Priority)

thread.setPriority(1)  // 1 ~ 10까지 우선순위를 정하며 10이 가장 높음

모든 스레드들은 기본적으로 5의 우선 순의를 할당받는다. 

싱글 코어의 경우 우선 순위가 높은 스레드가 실행 기회를 더 많이 가지기 때문에 더 빨리 작업이 끝난다.

하지만 멀티 코어의 경우 코어의 개수만큼 스레드가 병렬성으로 실행될 수 있기 때문에 코어의 수보다 많은 스레드가 실행되어야 우선순위의 영향을 받는다. 

코드로 제어 가능

 

- 순환 할당 방식 (Round-Robin) 

시간 할당량을 정해 하나의 스레드를 정해진 시간만큼 실행한다. (JVM안에서 이루어짐) 

코드로 제어 불가능 

 

4. 동기화 메소드와 동기화 블록 

싱글 스레드 프로그램에서는 한 개의 스레드만 사용하면 되나, 멀티 스레드 프로그램에서는 객체를 공유해서 작업해야 하는 경우가 있다. 이때, 각 스레드들은 다른 스레드에 영향을 주어 잘못된 결과를 도출할 수 있다.

이때 스레드가 사용 중인 객체에서 다른 스레드가 침범하지 못하도록 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 임계 영역 코드를 실행하지 못하도록 할 수 있다. 동기화 메소드를 만드는 방법은 메소드 선언에 synchronized 키워드를 붙인다. 해당 키워드는 인스턴스, 메소드 등 어디든 붙일 수 있다. 

public class Multi_thread  {
	int value = 0;
	//동기화 메소드
	public synchronized void setValue(int value) {
		this.value = value;
		System.out.println(Thread.currentThread().getName() + " value : " + this.value);
	}
	//동기화 블록
	public void setValue2(int value) {
		synchronized (this) {
			this.value = value;
		}
		System.out.println(Thread.currentThread().getName() + " value : " + this.value);
	}
	
}

 

5. 스레드 상태

스레드로 객체를 생성하면 먼저 "실행 대기" 상태로 들어간다. 실행 대기 상태란 아직 스케쥴링 되지 않고 실행을 대기하고 있는 상태이다. 스레드 스케쥴링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행한다.  이때 CPU를 점유하고 있는 스레드 상태를 "실행" 상태라 한다. 실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 스레드 스케쥴링에 의해 다시 실행 대기 상태로 돌아가 번갈아가며 스레드를 실행한다. 실행상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 되며 이 상태를 "종료" 상태라 한다.  

 

 5.1 스레드 상태 제어

실행중인 스레드의 상태를 변경할 수 있다. 

start() : 객체 생성 -> 실행 대기

yield() : 실행 대기 < -> 실행

sleep(), join(), wailt() : 실행 -> 일시 정지

interrupt(), notify(), notifyAll() : 일시정지 -> 실행 대기

stop() : 실행 -> 종료 

 

6. 스레드 그룹

스레드 그룹은 이름에서 유추할 수 있듯이 서로 관련 있는 스레드를 한꺼번에 관리할 수 있다.  

//스레드 그룹 생성
ThreadGroup rootGroup = new ThreadGroup(String name); 
ThreadGroup childGroup = new ThreadGroup(ThreadGroup root, String name);

//부모 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성 된다. 
//스레드를 그룹에 명시적으로 포함
Thread childGroup = new Thread(ThreadGroup rootGroup, Runnable target);

스레드 그룹의 interrupt() 메소드를 한 번만 호출하면 포함된 스레드를 한 번에 종료할 수 있다. 하지만 개별적으로 발생하는 예외 발생은 처리하지 않기 때문에 개별적 예외 처리가 필요하다. 

 

7. 스레드 풀

스레드가 급격하게 많아진다면 스레드 생성과 스케쥴링으로 인해서 CPU의 메모리 사용량이 늘어나고 각각의 애플리케이션 성능이 줄어들 것이다. 이를 막기 위해 스레드 풀을 생성하여 스레드 생성 최대 개수를 제한하고, 큐(Queue) 방식을 통해 스레드를 효율적으로 관리할 수 있다. 

 

7.1 스레드풀 생성

- newCachedThreadPool()

  • 자동으로 스레드 생성
  • 스레드가 60초 동안 작업을 하지 않으면 스레드를 종료하고 풀에서 제거
ExecutorService executorService = Executors.newCachedThreadPool();

- newFixedThreadPool(int nThreads)

  • 지정한 개수만큼 스레드 생성
  • 스레드가 작업을 하지 않아도 스레드 개수가 줄지 않음.
ExecutorService executorService = Executors.newFixedThreadPool(n);
//Runtime.getRuntime().avaliableProcessors(); 가능한 코어의 수

- ThreadPoolExecutor

//코어 스레드 수, 최대 스레드 수, 놀고 있는 시간, 노는 시간 단위, 작업 큐
ExecutorService threadPool = new ThreadPoolExecutor(3,200,60L,TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

 

7.2 스레드풀 종료

스레드 풀의 스레드는 데몬 스레드가 아니다. main 스레드가 종료되어도 계속 실행되고 있다. 그렇기 때문에 스레드 풀을 종료하여 모든 스레드를 종료해야만 한다. 

 

//모든 작업이 끝난 뒤 스레드를 종료한다. 
executorServiceWithCached.shutdown();

//작업 중인 스레드를 interrupt하여 작업을 중지하고 스레드 풀을 종료한다. 미처리된 작업의 목록이 리턴된다. 
List<Runnable> notFinish = executorServiceWithCached.shutdownNow();

//모든 작업처리를 지정 시간내에 못하면 작업 중인 스레드를 interrupt하고 false 리턴
boolean isFinish = executorServiceWithCached.awaitTermination(120, TimeUnit.SECONDS);

 

작업 생성

Runnable 또는 Callable 객체 

- 작업 처리 완료 후 리턴 값 유무의 차이

//Runnable 구현 클래스 
Runnable task1 = new Runnable() {
	@Override
    public void run() { 
    //작업
    }
}

//Callable 구현 클래스
Callable<T> task2 new Callable<T> {
	@Override
    public T call() throws Exception { 
    //작업
    return T;
    }
}

작업 처리 요청

ExecutorService의 작업 큐에 객체를 넣는 것이다. 

- execute() 

  • Runnable을 작업 큐에 저장
  • 리턴 값이 없으며 작업 처리 결과를 받지 못한다. 
  • 예외 발생시 스레드가 종료되고 해당 스레드는 제거된다. 

 

-submit()

  • Runnable 또는 Callable을 작업 큐에 저장
  • 리턴 값이 있으며 Future를 통해 작업 처리 결과를 얻을 수 있다. 
  • 예외 발생시 스레드가 종료되지 않고 다음 작업을 위해 재사용된다. (재생성하지 않아도 되기 때문에 자원 관리에 더 효율적) 
//리턴 값 없음
executorServiceWithCached.execute(task1);

//리턴 값 있음 
Future<Boolean> returnTask = executorServiceWithCached.submit(task2);

 

작업 완료 통보

Future

  • 지연 완료 객체
  • get() 메소드는 작업이 완료될 때까지 기다렸다가 최종 결과를 얻을 수 있다. 
  • V get(), V get(long timeout, TimeUnit unit)
    • 지정 시간 동안 작업이 완료되면 결과 V를 리턴하고 그렇지 않으면 Exception이 발생한다. 

 

작업 처리 결과를 외부 객체에 저장

스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 저장한다면 애플리케이션은 이 Result 객체를 활용할 수 있을 것이다. Result 객체는 두 개 이상의 스레드 작업을 처리할 목적으로 사용된다. 

 

public class Result {
	private int result;
	
	public int sum(int num) {
		return result += num;
	}
	
	public void showSum() {
		System.out.println("Sum Tmp : " + result);
	}
	
	public int getResult() { 
		return result;
	}
}
public class FirstThread  implements Runnable{
	Result result;
	
	public  FirstThread(Result result) {
		this.result = result;
	}
	
	@Override
	public void run() {
		int tmp = 0;
		for (int i=0; i<10; i++) {
			tmp += i;
			try {
				Thread.sleep(5);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Thread 1 : " + tmp);
		result.sum(tmp);
		result.showSum();
	}
}
public class SecondThread implements Runnable{
	Result result;
	
	public SecondThread(Result result) {
		this.result = result;
	}
	
	@Override
	public void run() {
		int tmp = 0;
		for(int i= 10; i<20; i++) {
			tmp += i;
		}
		System.out.println("Thread 2 : " + tmp);
		result.sum(tmp);
		result.showSum();
	}
}
public class ThreadTest {
	public static void main(String[] args) {
		//스레드 풀 생성
		//사용할 수 있는 코어 수만큼 스레드 생성
		ExecutorService executorService = Executors.newFixedThreadPool(
				Runtime.getRuntime().availableProcessors()
		);
		System.out.println("[작업 요청]");
		
		Result result = new Result();
		
		Runnable task1 = new FirstThread(result);
		Runnable task2 = new SecondThread(result);
		
	    Future<Result> future1 =  executorService.submit( task1, result );
	    Future<Result> future2 =  executorService.submit( task2, result);
	    
	    //main 스레드의 작업이 멈추지 않기 위해 새로운 스레드로 구성
	    executorService.execute(() -> {
	    	try {
				Result tmp = future1.get();
				tmp = future2.get();
				System.out.println("[작업 처리 완료]  " + result.getResult());
			} catch (Exception e) {
			}
	    });
	    
	    //작업 큐에 있는 모든 작업이 끝난 뒤 스레드 종료
	    executorService.shutdown();
	}
}
[작업 요청]
Thread 2 : 145
Sum Tmp : 145
Thread 1 : 45
Sum Tmp : 190
[작업 처리 완료]  190

 

작업 완료 순으로 통보

스레드의 작업 양에 따라 작업 처리 완료의 순서가 뒤바뀔 수 있다. 

처리 결과를 순차적으로 이용할 필요가 없다면 먼저 처리가 완료된 것 부터 결과를 받아서 활용할 수 있다. 

- CompletionService

poll() : 완료된 작업의 Future 가져옴. 완료된 작업이 없으면 즉시 null 리턴

take() : 완료된 작업의 Future 가져옴. 완료된 작업이 없다면 있을 때까지 블로킹됨. 

public class ThreadTest {
	public static void main(String[] args) {
		//스레드 풀 생성
		//사용할 수 있는 코어 수만큼 스레드 생성
		ExecutorService executorService = Executors.newFixedThreadPool(
				Runtime.getRuntime().availableProcessors()
				);
		System.out.println("[작업 요청]");

		Result result = new Result();

		Runnable task1 = new FirstThread(result);
		Runnable task2 = new SecondThread(result);

		Future<Result> future1 =  executorService.submit( task1, result );
		Future<Result> future2 =  executorService.submit( task2, result);

		//각 스레드의 리턴 객체 저장
		CompletionService<Result> completionService = new ExecutorCompletionService<Result>(executorService);

		//main 스레드의 작업이 멈추지 않기 위해 새로운 스레드로 구성
		executorService.submit(() -> {
			while (true) {
				try {
					//작업 처리가 된 결과를 받아옴.
					Future<Result> take = completionService.take();
					Result result2 = take.get();

					System.out.println("[작업 처리 계산 중]  " + result2.getResult());
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
			}
		});

		//스레드 강제 종료
		try {
			Thread.sleep(10000);
			executorService.shutdownNow();
		} catch (InterruptedException e) {
		}
	}
}

 

 

콜백 방식의 통보

콜백이란 작업이 끝나면 다음 작업을 미리 정의해놓고 작업이 끝나면 다음 메소드를 실행하는 기법이다. 자동 실행되는 메소드를 콜백 메소드라고 한다. 

java.nio.channels.CompletionHandler 인터페이스를 활용해서 만들 수 있다. 

public class Result {
	private int result;
	
	private static CompletionHandler<Result, String> completionHandler = new CompletionHandler<Result, String>() {
		//실패시 처리 작업
		@Override
		public void failed(Throwable exc, String attachment) {
			System.out.println("Fail");
		}
		//성공시 처리 작업
		@Override
		public void completed(Result result, String attachment) {
			System.out.println("Success");
			System.out.println("Result : " + result.getResult());
		}
	};
	
	public static CompletionHandler<Result, String> getCompletionHandler() {
		return completionHandler;
	}
	
	public int sum(int num) {
		return result += num;
	}
	
	public void showSum() {
		System.out.println("Sum Tmp : " + result);
	}
	
	public int getResult() { 
		return result;
	}
}
public class FirstThread  implements Runnable{
	Result result;
	
	public  FirstThread(Result result) {
		this.result = result;
	}
	
	@Override
	public void run() {
		int tmp = 0;
		for (int i=0; i<10; i++) {
			tmp += i;
			try {
				Thread.sleep(5);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Thread 1 : " + tmp);
		result.sum(tmp);
		
		//끝난 후 성공 콜백 실행
		result.getCompletionHandler().completed(result, null);
		
	}
}
public class SecondThread implements Runnable{
	Result result;
	
	public SecondThread(Result result) {
		this.result = result;
	}
	
	@Override
	public void run() {
		int tmp = 0;
		for(int i= 10; i<20; i++) {
			tmp += i;
		}
		System.out.println("Thread 2 : " + tmp);
		result.sum(tmp);
		//끝난 후 성공 콜백 실행
		result.getCompletionHandler().completed(result, null);
	}
}
public class ThreadTest {
	public static void main(String[] args) {
		//스레드 풀 생성
		//사용할 수 있는 코어 수만큼 스레드 생성
		ExecutorService executorService = Executors.newFixedThreadPool(
				Runtime.getRuntime().availableProcessors()
				);
		System.out.println("[작업 요청]");

		Result result = new Result();

		Runnable task1 = new FirstThread(result);
		Runnable task2 = new SecondThread(result);

		Future<Result> future1 =  executorService.submit( task1, result );
		Future<Result> future2 =  executorService.submit( task2, result);

		//각 스레드의 리턴 객체 저장
		CompletionService<Result> completionService = new ExecutorCompletionService<Result>(executorService);

		//main 스레드의 작업이 멈추지 않기 위해 새로운 스레드로 구성
		executorService.execute(() -> {
			try {
				//작업 처리가 된 결과를 받아옴.
				Future<Result> take = completionService.take();
				Result result2 = take.get();

				System.out.println("[작업 처리 계산 중]  " + result2.getResult());
			} catch (Exception e) {
				e.printStackTrace();
			}
		});

		//스레드  종료
		executorService.shutdown();
	}
}
[작업 요청]
Thread 2 : 145
Success
Result : 145
Thread 1 : 45
Success
Result : 190

 

 

 

 

참고 :

https://honbabzone.com/java/java-thread/

이것이 자바다

 

 

 

'Study > Java' 카테고리의 다른 글

[Java] Servlet(서블릿) 이란?  (0) 2022.03.01
[Java] 직렬화(Serialization)  (0) 2021.10.25
[Java] 파일 입출력 정리  (0) 2021.09.08
자바 2차원 배열 정렬하기  (0) 2020.09.01