본문 바로가기

BackEnd/Java

[java] 이것이 자바다 ch14 스레드(thread) 2

728x90
반응형

6. 스레드 동기화

◎ 동기화 메소드와 블록

   ▷ 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 합니다.

 

◎ 동기화 메소드 및 블록 선언

   ▷ 인스턴스와 정적 메소드에 synchronized 키워드를 붙입니다.

   ▷ 동기화 메소드를 실행 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀립니다.

   ▷ 메소드 일부 영역 실행 시 객체 잠금을 걸고 싶다면 동기화 블록을 만들어줍니다.

 

◎ 동기화 메소드 예제

1. Calculator 클래스
public class Calculator {
	private int memory;
	
	public int getMemory() {
		return memory;
	}
	
	// 동기화 메소드
	public synchronized void setMemory1(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);
		} catch(InterruptedException e) {}
		System.out.println(Thread.currentThread().getName() + ": " + this.memory);
	}
	
	// 동기화 블록을 포함하는 메소드
	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);
		}
	}
}​


▷ setMemory1과 2 모두 synchronized를 사용하지 않으면 마지막 저장된 값이 출력됩니다.

2. User1Thread 클래스
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);
	}
}​

 

3. User2Thread 클래스
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);
	}
}​

 

4. SynchronizedExample 메인 클래스
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();
	}
}

//	출력 : 
//	User1Thread: 100
//	User2Thread: 50​

 

◎ wait()와 notify()를 이용한 스레드 제어

   ▷ wait() : 자신의 스레드를 일시 정지

   ▷ notify() :  wait()에 의해 일시정지된 스레드 중 한 개를 실행 대기로 만듦

   ▷ notifyAll() :  wait()에 의해 일시정지된 모든 스레드를 실행 대기로 만듦

 

◎ wait(), notify()를 이용한 예제

1. WorkObject 클래스
public class WorkObject {
	public synchronized void methodA() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodA 작업 실행");
		notify();
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}
	
	public synchronized void methodB() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodB 작업 실행");
		notify();
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}
}​

 

2. ThreadA 클래스
public class ThreadA extends Thread {
	private WorkObject workObject;
	
	public ThreadA(WorkObject workObject) {
		setName("ThreadA");
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObject.methodA();
		}
	}
}​

 

3. ThreadB 클래스
public class ThreadB extends Thread {
	private WorkObject workObject;
	
	public ThreadB(WorkObject workObject) {
		setName("ThreadB");
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObject.methodB();
		}
	}
}​

 

4. WaitNotifyExample 메인 클래스
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();
	}
}​


▷ A와 B가 번갈아가면서 각각 10번씩 총 20번 출력됩니다.

 

 

7. 스레드 안전하게 종료하기

   ▷ 스레드 강제 종료 stop() 메소드 : deprecated(더 이상 사용하지 않음)

      ▶ stop 메소드는 리소스 정리가 되지 않아 따로 리소스 정리 코드가 필요합니다.(조건 이용)

   ▷ 스레드를 안전하게 종료하려면 사용하던 리소스(파일, 네트워크 연결)를 정리하고 run() 메소드를 빨리 종료해야 합니다.

 

   ▷ while 문으로 반복 실행시 조건을 이용해 run() 메소드 종료를 유도합니다.

 

◎ 메소드를 만들어 정지시키는 예제

1. PrintThread 클래스
public class PrintThread extends Thread {
	private boolean stop;
	
	public void setStop(boolean stop) {
		this.stop = stop;
	}
	
	@Override
	public void run() {
		// stop 실행 될때까지 계속 스레드 실행
		while(!stop) {
			System.out.println("실행 중");
		}
		// stop이 되면 리소스 정리 후 실행 종료
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}​

 

2. SafeStopExample 예제
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);
	}
}​

 

▷ '실행 중' 이라는 글자가 3초동안 출력되다가 stop이 true 값이 대입되면서 반복문을 벗어나 멈춥니다.

 

 

◎ interrupt() 메소드 이용

   ▷ 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외 발생

      ▶ sleep()으로 일시정지가 되어야지만 인터럽트가 사용 가능합니다.

   ▷ 예외 처리를 통해 run() 메소드를 정상 종료

   ▷ Thread의 interrupted()와 isInterrupted() 메소드는 interrupt() 메소드 호출 여부를 리턴

      ▶ Thread.interrupted() : true, false (정적 메소드)

      ▶ objThread.isInterrupted() : true, false (동적 메소드) 

 

  interrupt를 만들어 반복을 벗어나게 하는 예제 

1. PrintThread 클래스
public class PrintThread extends Thread {
	@Override
	public void run() {
		// stop 실행 될때까지 계속 스레드 실행
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}			
		} catch(InterruptedException e) {
		}
		
		// stop이 되면 리소스 정리 후 실행 종료
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}​

 

2. InterruptExample 메인 클래스
public class InterruptExample {

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

 

▷ '실행 중' 이라는 글자가 1초동안 출력되다가 interrupt를 발생시키고 while문을 벗어나도록 만듭니다.

 

◎  interrupt를 만들어 반복을 벗어나게 하는 예제 2

public class PrintThread extends Thread {
	@Override
	public void run() {
		// stop 실행 될때까지 계속 스레드 실행
		while(true) {
			System.out.println("실행 중");
			if(Thread.interrupted()) {
				break;
			}
		}			
		
		// stop이 되면 리소스 정리 후 실행 종료
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}


 PrintThread의 코드를 위와 같이 구성해도 동일한 결과 값이 나타납니다.

 

 

8. 데몬 스레드

   ▷ 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드

   ▷ 주 스레드가 종료되면 데몬 스레드도 따라서 자동 종료

   ▷ 데몬 스레드 적용 예: 워드프로세서의 자동 저장, 미디어플레이어의 동영상 및 음악 재생, 가비지 컬렉터

   ▷ 주 스레드가 데몬이 될 스레드의 setDaemon(true) 호출

 

1. AutoSaveThread 클래스
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();
		}
	}
}​


2. DaemonExample 메인클래스
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("메인 스레드 종료");
	}
}

//	출력 : 
//	작업 내용을 저장함.
//	작업 내용을 저장함.
//	메인 스레드 종료​


▷ AutoSaveThread를 데몬 스레드로 만들어줍니다.
 1초에 한 번씩 데몬 스레드가 실행되고 3초 뒤에 메인 스레드가 종료되면 자동으로 데몬 스레드도 종료됩니다.

 

 

9. 스레드풀

   ▷ 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식

 

◎ 스레드풀로 작업 처리 제한하기

   ▷ 작업 처리에 사용되는 스레드 개수를 제한하고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식

   ▷ 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리

   ▷ 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션 성능의 급격한 저하 방지

 

◎ 스레드풀 생성

   ▷ java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공

   ▷ Executors의 다음 두 정적 메소드를 이용하면 스레드풀인 ExecutorService 구현 객체를 만들 수 있습니다.

NO 메소드명(매개변수) 초기 수 코어 수 최대 수
1 newCachedThreadPool() 0 0 Integer.MAX_VALUE
2 newFixedThreadPool(int nThreads) 0 생성된 수 nThreads

 

   ▷ 초기 수 : 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수

   ▷ 코어 수 : 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수

   ▷ 최대 수 : 증가되는 스레드의 한도 수

 

◎ 스레드풀 종료

   ▷ 스레드풀의 스레드는 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남음

NO 리턴 타입 메소드명
(매개변수)
설명
1 void shutdown() 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을
처리한 뒤에 스레드풀을 종료시킵니다.
2 List<Runnable> shutdownNow() 현 작업 처리 중인 스레드를 interrupt해서 작업을 중지시키고 스레드풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록입니다.

 

◎ 스레드풀 종료 예제

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ExecutorServiceExample {
	public static void main(String[] args) {
		
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		// 최대 스레드 개수 5개 고정
		
//		ExecutorService executorService = new ThreadPoolExecutor();
	// 생성된 스레드풀의 초기 수와 코어 수는 0개 이고, 작업 개수가 많아지면 새 스레드를 생성시켜
	// 작업을 처리한다. 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.
		
//		ExecutorService executorService = new ThreadPoolExecutor(
//				3,					// 코어 스레드 개수
//				100,					// 최대 스레드 개수
//				120L,					// 놀고 있는 시간
//				TimeUnit.SECONDS,			// 놀고 있는 시간 단위
//				new SynchronousQueue<Runnable>()	// 작업 큐
//				);
		
		executorService.shutdown();
	}
}

 

작업 생성과 처리 요청

   ▷ 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현

   ▷ 작업 처리 요청 : ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위

 

◎ Runnable 익명 구현 예제

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;
			// 해킹 방지때문에 고정을 시킵니다.
			// static이 있으면 변경 자체가 불가능 합니다.
			// public만 있으면 for문 한 번 실행할 동안 변수 변경 불가능합니다.
			
			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();
	}
}


//	출력 : 
//	[pool-1-thread-5]admin@my.com ==> member998@my.com: 신상품 입고
//	[pool-1-thread-4]admin@my.com ==> member997@my.com: 신상품 입고
//	[pool-1-thread-2]admin@my.com ==> member996@my.com: 신상품 입고
//	[pool-1-thread-1]admin@my.com ==> member995@my.com: 신상품 입고
//	[pool-1-thread-3]admin@my.com ==> member999@my.com: 신상품 입고

 

◎ Callable 익명 구현 예제

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

public class CallableSubmitExample {

	public static void main(String[] args) {

		//ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		
		// 계산 작업 생성 및 처리 요청
		for(int i=1; i<=100; i++) {
			final int idx = i;
			Future<Integer> future = executorService.submit(new Callable<Integer>() {
				
				@Override
				public Integer call() throws Exception{
					int sum = 0;
					for(int i=1; i<=idx; i++) {
						sum += i;
					}
					
					Thread thread = Thread.currentThread();
					System.out.println("[" + thread.getName() + "] 1~"
                    						+ idx + "합 계산");
					return sum;
				}
			});
			
			try {
				int result = future.get();
				System.out.println("\t리턴값: "+result);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		// ExecutorService 종료
		executorService.shutdown();
	}
}

//	출력 : 
//	[pool-1-thread-3] 1~98합 계산
//	리턴값: 4851
//	[pool-1-thread-4] 1~99합 계산
//	리턴값: 4950
//	[pool-1-thread-5] 1~100합 계산
//	리턴값: 5050

 

sleep을 사용하면 InterruptedException 예외를 꼭 사용해야합니다.

 

여러가지 방법들을 통해 스레드를 구현하는 방법에 대해서 알아보았습니다.

스레드가 중요하다고 하는데 어떻게 사용될지는 앞으로 더 공부해보겠습니다!!

 

많은 분들의 피드백은 언제나 환영합니다! 많은 댓글 부탁드려요~~

 

728x90
반응형