혼자 공부하는 자바 - 12장. 스레드

youngtae·2023년 4월 25일
0

자바 이론

목록 보기
11/12
post-thumbnail

멀티 스레드

스레드

운영체제는 두가지 이상의 작업을 동시에 처리하는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킴

  • 하나의 스레드는 하나의 코드 실행 흐름
  • 멀티 프로세스 vs 멀티 스레드
    • 멀티 프로세스

      운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세스는 독립적
      → 하나의 프로세스에서 오류 발생해도 다른 프로세스에 영향 미치지 않음

    • 멀티 스레드

      하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향 미침


메인 스레드

  • 자바의 모든 애플리케이션은 메인 스레드가 main() 메서드를 실행하면서 시작
  • 메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행
    즉, 멀티 스레드를 생성해서 멀티 태스킹 수행
    • 싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료
    • 멀티 스레드 애플리케이션에서는 실행중인 스레드가 하나라도 있다면, 프로세스 종료되지 않음

작업 스레드 생성과 실행

  • 멀티 스레드 애플리케이션을 개발하려면 먼저 몇개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드 생성해야 함
  • 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요
  • java.lang.Thread 클래스를 직접 객체화 하는 방법과 Thread클래스를 상속해서 하위 클래스를 만들어 생성하는 방법이 있음

Thread 클래스로부터 직접 생성

  • Runnable을 매개값으로 갖는 생성자 호출
    Thread thread = new Thread(Runnable target);
  • Runnable은 인터페이스 타입이기 때문에 구현 객체 만들어 대입해야 함
  • Runnable에는 run() 메서드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드 작성
    class Task implements Runnable {
    	public void run() {
    		스레드가 실행할 코드;
    	}
    }
  • Runnable 구현 객체를 생성 후, 이것을 매개값으로 Thread 생성자 호출해야 작업스레드 생성
  • 일반적으로 코드 절약위해 Thread 생성자 호출할 때 Runnable 익명 객체를 매개값으로 사용
    Thread thread = new Thread(new Runnable() {
    	public void run() {
    		스레드가 실행할 코드;
    	}
    });
  • start() 메서드 호출해야만 메서드 실행됨 thread.start();
  • 메인스레드만 사용하는 경우
    import java.awt.Toolkit;
    
    public class BeepPrintExample1 {
    	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) {}
    		}
    	}
    }
  • 메인스레드와 작업 스레드 동시 실행
    // 비프음 실행 작업
    import java.awt.Toolkit;
    
    public class BeepTask implements Runnable {	
    	public void run() {		
    		Toolkit toolkit = Toolkit.getDefaultToolkit();	
    		for(int i=0; i<5; i++) {		
    			toolkit.beep();
    			try { Thread.sleep(500); } catch(Exception e) {}
    		}
    	}
    }
    
    // 메인스레드
    public class BeepPrintExample2 {
    	public static void main(String[] args) {
    		Runnable beepTask = new BeepTask();
    		Thread thread = new Thread(beepTask);
    		thread.start();
    		
    		for(int i=0; i<5; i++) {
    			System.out.println("띵");
    			try { Thread.sleep(500); } catch(Exception e) {}
    		}
    	}
    }

Thread 하위 클래스로부터 생성

  • Runnable로 실행 작업 만들지 않고, Thread 하위 클래스로 작업 스레드를 정의하면서 작업 내용 포함 가능
  • Thread 클래스 상속 후 run()메서드를 오버라이딩해서 스레드가 실행할 코드 작성
    public class WokerThread extends Thread {
    	@Override
    	public void run() {
    		스레드가 실행할 코드;
    	}
    }
    Thread thread = new WokerThread();
    
    // 코드 절약 위해 익명 객체로 작업 스레드 생성
    Thread thread = new Thread() {
    	public void run() {
    		스레드가 실행할 코드;
    	}
    
  • 스레드 실행 thread.start();
  • 메인 스레드와 작업스레드 동시 실행
    // 비프음 스레드
    import java.awt.Toolkit;
    
    public class BeepThread extends 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) {}
    		}
    	}
    }
    
    // 실행 코드
    public class BeepPrintExample4 {
    	public static void main(String[] args) {
    		Thread thread = new BeepThread();
    		thread.start();		
    		
    		for(int i=0; i<5; i++) {
    			System.out.println("띵");
    			try { Thread.sleep(500); } catch(Exception e) {}
    		}
    	}
    }

스레드 이름

  • 메인 스레드는 main이라는 이름 가지고, 직접 생성한 스레드는 자동적으로 Thread-n이라는 이름으로 설정
  • 이름 변경 : thread.setName(”스레드이름”);
  • 이름 알고 싶을 경우 : thread.getName();
  • setName(), getName()은 인스턴스 메서드이므로 객체 참조 필요,
    참조 가지고 있지 않다면 정적 메서드인 currentThread()이용
    Thread thread = Thread.currentThread();

동기화 메서드

  • 스레드들이 객체를 공유해서 작업해야 하는 경우, A가 사용하던 객체를 B가 변경할 수 있기 때문에 다른 결과를 산출할 수도 있기 때문에 주의해야 함
  • 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없게 해야 함
  • 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역
  • 자바는 임계 영역 지정을 위해 동기화 메서드를 제공
  • 메서드 선언에 synchronized 키워드 붙임
    public synchronized void method() {
    	임계영역;
    }
  • 동기화 메서드는 실행 즉시 잠금되고, 실행 종료되면 풀림

스레드 제어

스레드 상태

  • 스레드 객체 생성하고 start() 호출하면 바로 실행되는것이 아니라 실행 대기 상태가 됨
  • 실행 대기 상태에 있는 스레드 중에 운영체제는 하나의 스레드를 선택하고 CPU가 run() 메서드 실행 → 이때를 실행 상태
  • 실행 상태에서 run() 메서드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드 실행 멈춤
    → 이때를 종료 상태
  • 경우에 따라서 실행 상태에서 일시정지 상태로 가기도 함. 이경우 바로 실행 상태로 돌아갈 수 없고, 실행 대기 상태로 가야함

스레드 상태 제어

  • 실행 중일 스레드의 상태를 변경하는 것을 스레드 상태 제어
  • 멀티 스레드 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요
메서드설명
interrupt()일시 정지 상태의 스레드에서 InterruptedException을 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다.
sleep(long millis)주어진 시간 동안 스레드를 일시 정지 상태로 만듭니다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다.
stop()스레드를 즉시 종료합니다. 불안전한 종료를 유발하므로 사용하지 않는 것이 좋습니다.

주어진 시간 동안 일시 정지

  • 일정시간 스레드를 멈추고 싶다면 sleep() 메서드를 사용
  • 매개값에는 밀리세컨드 단위로 시간 입력 (1초= 1000)
  • 일시정지 상태에서 주어진 시간 되기전에 interrupt() 메서드가 호출되면 InterruptedException이 발생하기 때문에 예외처리 필요
  • 3초 주기로 10번 비프음 발생
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); // 3초 일시정지
			} catch(InterruptedException e) {			
			}		
		}	
	}
}

스레드 안전한 종료

  • Thread는 즉시 종료하기 위해 stop()메서드를 제공하고 있는데, stop() 메서드로 갑자기 종료하게 되면 사용 중이던 자원들이 불안전한 상태로 남겨지기 때문에 이제 사용되지 않음
  • stop 플래그를 이용하는 방법
    • 스레드는 run()메서드가 끝나면 자동으로 종료되므로, stop 플래그를 이용해서 run()메서드의 종료를 유도

      public class XXXThread extends Thread {
      	private boolean stop; // stop 플래그 필드
      
      	public void run() {
      		while(!stop) {
      			스레드가 반복 실행하는 코드;
      		}
      		// 스레드가 사용한 자원 정리
      	}
      }
    • stop 필드가 true일 경우 while문을 빠져나오고 스레드가 사용한 자원을 정리

      → run() 메서드가 끝나게 되므로 스레드는 안전하게 종료

  • interrupt 메서드 이용하는 방법
    • interrupt() 메서드는 스레드가 일시정지 상태에 있을 때 InterruptedException을 발생시키는 역할을 함. 이를 이용하면 run()메서드 정상 종료 가능

      ThreadA가 ThreadB의 interrupt() 메서드를 실행하게 되면 ThreadB가 sleep() 메서드로 일시 정지 상태가 될 때 ThreadB에서 InterruptedException이 발생하여 예외처리 블록으로 이동

      → ThreadB는 while문 빠져나와 run()메서드 정상 종료

      // 1초후 출력 스레드 중지
      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 메서드 호출
      	}
      }
      
      // 무한 반복해서 출력하는 스레드
      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("실행 종료");
      	}
      }
    • 주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메서드가 실행되면 즉시 InterruptedException이 발생하지 않고, 스레드가 일지 정지 상태가 되면 발생

    • 따라서 일지 정시 상태가 되지 않으면 interrupt() 메서드 호출은 의미 없음

      → 일시정지 만들기 위해 Thread.sleep(1) 사용

    • 일시정지 만들지 않고 interrupt() 호출

      public class PrintThread extends Thread {
      	public void run() {	
      		while(true) {
      			System.out.println("실행 중");
      			if(Thread.interrupted()) { // interrupted 되었는지 확인
      				break;
      			}
      		}
      		
      		System.out.println("자원 정리");
      		System.out.println("실행 종료");
      	}
      }

데몬 스레드

  • 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
  • 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료
  • 데몬 스레드로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출
    public static void main(String[] args) {
    		AutoSaveThread thread = new AutoSaveThread();
    		thread.setDaemon(true);
    		thread.start();
    		...
    }  // AutoSaveThread가 데몬이 됨
  • 주의할 점은 start() 메서드가 호출되고 나서 SetDaemon(true)를 호출하면 IllgalThreadStateException이 발생하기 때문에 start() 호출 전에 해야함
// 1초 주기로 save() 메서드 호출하는 데몬 스레드
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();
		}
	}
}

// 메인스레드 코드
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("메인 스레드 종료");
	}
}
profile
나의 개발기록

0개의 댓글