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); } } }
2. User1Thread 클래스
▷ setMemory1과 2 모두 synchronized를 사용하지 않으면 마지막 저장된 값이 출력됩니다.
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 예외를 꼭 사용해야합니다.
여러가지 방법들을 통해 스레드를 구현하는 방법에 대해서 알아보았습니다.
스레드가 중요하다고 하는데 어떻게 사용될지는 앞으로 더 공부해보겠습니다!!
많은 분들의 피드백은 언제나 환영합니다! 많은 댓글 부탁드려요~~
'BackEnd > Java' 카테고리의 다른 글
[java] 이것이 자바다 ch15 컬렉션 2 (0) | 2023.01.26 |
---|---|
[java] 이것이 자바다 ch15 컬렉션 1 (0) | 2023.01.25 |
[java] 이것이 자바다 ch14 스레드(thread) (2) | 2023.01.25 |
[백준 문제 2941번] 크로아티아 알파벳 문제 (0) | 2023.01.24 |
[백준 문제 1152번] 단어의 개수 문제 (0) | 2023.01.22 |