[TIL]20211024

박창현·2021년 10월 24일
0

TODAY I LEARNED

목록 보기
52/53

자바

스레드

프로세스

운영체제에서는 실행중인 하나의 애플리케이션을 프소세스라고 한다.
만약 메모장 2개를 실행하면 메모장 에플리케이션 2개의 프로세스를 실행한 것이다.

스레드

한 가지 작업을 실해하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름이기 때문에 한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이 생긴다는 의미이다.
또한, 스레드 사이에서 객체들을 공유해서 사용할 수도 있다.

멀티 태스킹

운영체제가 두 가지 이상의 작업을 동시에 처리하는 것. 즉, 병렬로 연결시켜 실행하는 것이다.
하지만 멀티 태스킹이 멀티 프로세스를 뜻하는 것은 아니다. 하나의 프로세스 내에서도 스레드를 통해 여러 작업을 동시에 처리할 수 있다.
동영상 플레이어의 경우 하나의 프로세스에서 기본적으로 영상 재생과 음성 재생, 두 가지 스레드 즉, 멀티 스레드가 발생한다고 볼 수 있다.

운영체제에서 할당받은 메모리를 가지고 실행하기에 각 프로세스는 서로 독립적이다.
하지만 스레드의 경우 프로세스에 할당해준 한정된 메모리를 공유하기에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 준다.

thread 클래스

이 클래스는 Runnable을 매개갑으로 갖는다.
Thread thread = new Thread(Runnable target);
이때 Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야한다.
구현 클래스에선 run()을 재정의해서 작업 스레드가 실행할 코드를 만들어야함.

즉, 이런 식이다.

class Task implements Runnable {
  public void run() {
    //스레드가 실행할 코드
  }
}

Runnable task = new Task();
Thread thread = new Thread(task);

더 간단히 만들려면

Thread thread = new Thread( new Runnable() {
  public void run() {
    //스레드가 실행할 코드
  }
} );

이러한 익명 객체 생성 방식으로도 많이 사용된다고 한다.

그리고 작업 스레드는 생성즉시 실행되는 것이 아니라, thread.start(); 처럼 start()메소드를 실행해야한다.

import java.awt.*;
public class Main {
	  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.start();
		 
		 for(int i = 0; i<5; i++) {
			 System.out.println("띵 ");
			 try {Thread.sleep(500);} catch(Exception e) {}
		 }
	  }
}

스레드를 이용해서 비프음과 출력이 각각의 클래스에서 동시에 작동되도록 하는 코드이다.

또한, Runnable인터페이스로 작업내용을 만들지 않고, Thread의 하위클래스로 작업 스레드를 정의해서 사용해도 된다.

public class WorkerThread extends Thread {
	@Override
    public void run() {
    	//스레드가 실행할 코드
    }
}
Thread thread = new WorkerThread();

// 상속 + 익명 객체이용
import java.awt.*;
public class Main {
	  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) {}
		 }
	  }
}

이런식으로.

동기화 메소드

여러 스레드에서 같은 객체(공유 객체)를 사용할 떄 예상치못한 경우가 발생한다. user1스레드가 A라는 객체의 a라는 값을 1이라고 정하고 2초를 기다리는 동안 user2스레드가 A라는 객체의 a라는 값을 2라고 바꿔놓고 2초간 기다린다면, 전체 2초 후에 user1이나 user2나 모두 a값을 2로 가지게 되어 user1이 원하던 값을 나중에는 얻을 수 없게 된다.

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역 이라고 하고, 이를 지정하기 위해 동기화 메소드를 사용해 이 문제를 해결한다.

스레드가 객체내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 한다.

public synchronised void method() {
  //임계영역, 단 하나의 스레드만 실행
}

동기화 메소드는 메소드 전체내용이 임계 영역이므로 메소드 실행즉시 객체에는 잠금이 일어나고, 메소드 종료시 잠금이 풀린다.
만약, 동기화 메소드가 여러개 있다면, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론, 다른 동기화 메소드들도 실행할 수 없다.
하지만 다른 스레드에서 일반 메소드는 실행이 가능하다.

만약 위의 A객체의 예를 여기에 적용하면 setMemory()라는 메소드를 synchronized로 만든다면, user1 스레드는 2초후에 1이라는 값을 출력하고 user2는 앞의 스레드가 끝나고 2초 후에 2라는 값을 출력한다.

스레드 상태

스레드는 start()메소드가 호출되면 실행 대기 상태가 된다. 이후 운영체제가 하나의 스레드를 선택해 CPU가 run()메소드를 실행하도록 하는데, 이때가 실행 상태 이다. 이 상태일때 스레드는 run()메소드가 끝나기전 다시 실행 대기 상태로 돌아갈 수 있다.
이렇게 실행 대기와 실행 상태를 번갈아 가면서 스레드마다의 run()메소드를 조금씩 실행 할 수 있다. 경우에 따라 일시 정지 상태에 가기도 하는 데 이 상태는 스레드가 실행할 수 없는 상태이다. 이때는 바로 실행 상태가 될 수 없고, 실행 대기 상태를 거쳐야 한다.

스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다.

스레드 상태 제어 메소드

interrupt(): 일시 정지 상태의 스레드에서 Interruptedexception을 발생시켜, 예외 처리 코드(catch)에서 실행 대기로 가거나 종료 상태로 갈 수 있도록 한다.
sleep: 주어진 시간동안 스레드를 일시 정지 상태로 만듭니다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다.

만약 sleep을 이용해 정지상태에 있을 때에 주어진 시간이 되기전에 interrupt()메소드가 호출되면 InterruptedException이 발생하기에 이에 관한 예외처리도 필요하다.

스레드의 안전한 종료

스레드는 run() 메소드가 끝나면 자동적으로 종료되므로, 메소드가 정상적으로 종료되도록 유도하는 것이 중요하다.

1. stop 플래그를 이용하는 방법.

~~~
boolean stop= false;
public void run() {
while(!stop){
  // 반복할 코드 작성.
  }
}

이런 식으로 boolean형의 stop플래그를 이용해서 종료시킬 수 있다.

이를 구현한다면 아래코드와 같다.

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

public class PrintThread extends Thread {
	private boolean stop;
	
	public void setStop(boolean stop) {
	  this.stop = stop;
	}
	
	public void run() {	
		while(!stop) {
			System.out.println("실행 중");
		}	
		System.out.println("실행 종료");
	}
}

2. interrupt() 메소드를 이용하는 방법

interrupt() 메소드는 스레드가 일시정지 상태에 있을 때 InterruptedException을 발생시키는 역할을 한다.
이를 이용하면 run()메소드를 정상 종료할 수 있습니다.

Class ThreadA
~~
ThreadB threadB = new ThreadB();
threadB.start();
~~~
threadB.interrupt();
------------------------------------------------------------
Class ThreadB
public void run() {
  try{
  while(true) {
    ~~~~
    Thread.sleep(1);
  }
  }catch(InterruptedException e) {
  }
  //스레드가 사용한 자원 정리.
}

ThreadA가 ThreadB의 interrupt() 메소드를 실행하게 되면 ThreadB가 sleep() 메소드로 일시 정지 상태가 될 때 ThreadB에서 InterruptedException이 발생하여 예외 처리(catch) 블록으로 이동합니다.
결국, ThreadB는 while문을 빠져나와 run() 메소드를 정상 종료하게 된다.
또한 주목할만한 점이 있다. 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException이 발생하지 않고, 스레드가 미래에 일시 정지상태가 되면 발생한다. 따라서 스레드가 일시 정지 상태가 되지 않으면 interrupt() 메소드 호출은 아무 의미가 없어진다. 그렇기에 짧은 시간이나마 일시 정지시키기 위해 Thread.sleep(1)을 사용한다.

이 방법을 사용하지 않는다면, boolean연산자를 리턴하는 Thread.interrupted()를 이용할 수도 있다.

while(true) {
  if(Thread.interrupted()) {
    break;
  }
}

데몬 스레드

이 스레드는 주 스레드의 작업을 돕는 보조역할을 한다.
thread.setDaemon(true); 를 이용해 설정할 수 있고, 실행은 thread.start(); 을 이용하지만, 주 스레드가 종료될 때 자동으로 종료된다.

profile
개강했기에 가끔씩 업로드.

0개의 댓글