너굴 개발 일지

TIL_210331_Thread (스레드) 본문

Java

TIL_210331_Thread (스레드)

너굴냥 2021. 3. 31. 22:18

Process (프로세스)

간단히 말해서 '실행중인 프로그램'으로 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.

프로세스 = 프로그램 수행하는데 필요한 데이터와 메모리 등의 자원 + 스레드

 

 

Thread (스레드)

실제로 작업을 수행하는 것을 의미하며 모든 프로세스에는 최소 1 스레드가 존재한다. 이걸 '싱글 스레드'라고 부르며 둘 이상의 스레드를 가진 프로세스는 '멀티스레드 프로세스'라고 한다.

예시를 들어 햄버거와 김밥을 만드는 주방에 비유한다면  ①햄버거 만드는 파트 ②김밥 만드는 파트 총 두개의 프로세스가 진행이 되며 햄버거 파트에서는 ⅰ)패티 굽는 과정 ⅱ) 햄버거 야채 다듬는 과정들이 있으며 햄버거 프로세스의 스레드라고 보면 되겠다.

스레드를 사용하는 이유에는 메인 스레드의 일을 분산시키고자 하는 것도 있다.

스레드는 메서드의 개념으로 main 메서드의 작업을 수행하는 main 스레드, run() 메서드가 있다. 

 

 

멀티쓰레딩의 장단점

( 예를 들면 카톡을 보낼 때 파일을 보내는 동안 텍스트를 전송할 수도 있다.)

 

 

장점

- CPU 사용률을 향상시킨다.

- 사용자에 대한 응답성이 향상된다.

- 작업이 분리되어 코드가 간결해진다.

 

단점

- 동기화 주의 (스레드들이 동시에 발생하면 오류 발생 가능성 커져)

- 교착상태 발생 (두 스레드가 자원을 점유한 상태에서 서로 자원을 바꿔 사용해야 할 때 기다리느라 진행이 멈춘 상태)

- 스레드가 많을 수록 특정 스레드가 작업할 기회가 적어지면서 기아상태가 될 수도 있다. (비효율적)

 

 

 

 

스레드의 구현과 실행

 

1-1 Thread 클래스를 상속 - Thread 클래스의 run() 메서드를 오버라이딩

    2 Thread 파생 클래스의 인스턴스 생성 후 start() 메서드 필수 호출 !

 

 

2-1 Runnable 인터페이스를 구현 - Runnable 인터페이스의 추상메서드 run()을 강제구현

    2 Runnable 인터페이스 구현한 클래스의 객체 생성

    3 Thread 클래스의 객체 만들고 객체의 매개변수를 Runnable 구현 클래스의 객체 대입 

    4 Thread 클래스 객체로 start() 메서드 필수 호출 !

    ※Runnable 인터페이스에는 추상메서드인 run()만 있기에 start() 호출이 불가하여

       스레드 인스턴스 생성할 때마다 매개변수로 Runnable 구현한 클래스 객체를 넣어줘야 한다.

 

 

Thread 클래스보단 Runnable 인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있어서 보다 객체지향적인 방법이다.

그리고 start()의 역할은 새로운 쓰레드가 작업을 실행하는데 필요한 콜스택을 생성한 다음 run()을 호출해주는 것으로 생성된 콜스택에 run()이 첫번째로 올라가게 된다. 그렇다고 start()를 호출한다고 해서 바로 실행되는 것은 아니며 여러 스레드 파생 클래스가 있을 때 어떤 게 먼저 실행될 지는 OS 스케줄러에 의해 결정된다.

 

예시를 들면 다음과 같다.

package com.bjy;


public class MyThread extends Thread { // start() 호출 => 스레드 구동 

	public MyThread() {
		
	}
	
	public void run() { 
		// 원하는 작업 내용
		for(int i=0;i<100;i++) {
			System.out.println("인덱스 : " + i);
			
		}
		
	}
	
	
	
}
package com.bjy;

public class MyRunnable implements Runnable{

	public MyRunnable() {
		
	}
	
	public void run() {
		// 작업내용
		for(int i=0;i<100;i++) {
			System.out.println("가짜 : " + i);
		}
	}
	

}
package com.bjy;


public class MainClass {

	public static void main(String[] args) {
		System.out.println("메인 시작");
		
		MyRunnable rc = new MyRunnable();
		Thread t1 = new Thread(rc);
		t1.start();
		
		MyThread mt = new MyThread();
		mt.start();
		
		System.out.println("메인 종료");
		

	}

}

 

두 클래스를 main 클래스에서 실행시 결과

결과를 보면 main 메서드의 과정이 종료되고 MyRunnable 클래스의 run(), MyThread의 run()이 실행되는 것을 볼 수 있다. (출력값이 많아서 잘랐지만 콘솔창을 쭉 내리면 실행된 결과를 볼 수 있다)

이와 같이 main 스레드가 끝났어도 다른 스레드가 진행되고 있기에 프로그램이 종료되지 않는 것을 알 수 있다.

 

결론적으론 실행 중인 사용자 스레드가 하나도 없을 때 프로그램은 종료된다. 

 

 

 

 

Thread Class Method

 

1. getName() 

   Thread 클래스의 이름을 불러오는 메서드로 Thread Class에는 private String name = ""; 변수가 있으며 

   Thread 파생 클래스 생성시, String 매개변수 생성자 안에 super(string)을 넣게 되면 원하는 이름으로 지을 수 있다.

   이름은 private 변수이기에 gettet메서드인 getName()을 호출하면 스레드 클래스의 이름을 불러올 수 있다.

   이름 설정을 따로 하지 않고 getName()을 호출하면 "Thread-n"이라는 이름으로 설정된다.

package com.bjy;

public class ThreadName extends Thread {   // Thread => private String name = "";

	public ThreadName() {
		
	}
	
	public ThreadName(String name) {
		super(name);
	}
	
	public void run() {
		int i = 0;
		while(i<5) {
			System.out.println(getName() + i);
			i++;
		}
	}

}

 

main 클래스에서 ThreadName 클래스 객체 생성 후 스레드 실행 시 getName() 메서드 호출로 해당 스레드의 이름이 출력되는 것을 볼 수 있다.

 

 

 

2. static void sleep(long millis)

   말그대로 잠시 잠재운다는 뜻으로 지정된 시간동안 스레드를 멈추게 한다.

   sleep()에 의해 일시정지 상태가 된 스레드는 지정된 시간이 지나거나 interrupt()가 호출되면

   InterruptedException이 발생되어 잠에서 깨어나 실행대기 상태가 된다.

   그래서 sleep()을 호출할 때는 try-catch문으로 예외처리를 해줘야 한다.

package com.bjy;

public class SleepThread extends Thread{

	public SleepThread() {
		
	}
	
	public void run() {
		System.out.println("start thread~~");
		
		System.out.println("Thread zzz...");
		try {
			Thread.sleep(3000);
			// 스레드를 잠시 잠재웠다 깨운다 
		} catch (InterruptedException e) {
			System.out.println(e.getMessage());
		}
		System.out.println("wake thread~~");
	}

}

실제로 실행시켜보면 "Thread zzz..." 문장 출력 후 약 3초 뒤에 스레드가 다시 실행되면서 "wake thread~~" 문장이 출력되는 것을 볼 수 있다.

 

 

 

3. void join() / (long millis)

   쓰레드 자신이 하던 작업을 잠시 멈추고 다른 스레드의 작업을 기다리는 메서드로 시간을 지정하지 않으면

   해당 스레드가 작업을 모두 마칠 때까지 기다리게 된다.

 

  try {
    th1.join();             // 현재 실행중인 쓰레드가 쓰레드 th1 작업이 끝날때까지 기다린다.
   } catch(InterruptedException e) {}

위의 SleepThread를 main 클래스에서 실행시 join()을 호출한 코드와 결과는 다음과 같다. 

package com.bjy;

public class MainClass {

	public static void main(String[] args) { // 주 스레드
		System.out.println("주 스레드 시작");

		SleepThread t3 = new SleepThread();
		t3.start();
		try {
			t3.join();
			// 해당 쓰레드가 끝날 때까지 다른 스레드가 기다려준다 
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println("주 스레드 종료");

	}

}

위의 결과와 반대로 SleepThread가 종료되어야 main 스레드가 종료된 것을 볼 수 있다.

 

 

 

Thread Priority

스레드는 우선순위라는 속성(멤버변수)를 가지는데, 우선순위의 값에 따라 스레드가 얻는 실행 시간이 달라진다.

즉 작업 중요도에 따라 우선순위를 다르게 지정해 더 많은 작업 시간을 갖도록 한다.

 

스레드의 우선 순위 정하기

void setPriority(int newPriority)                      쓰레드의 우선순위를 지정한 값으로 변경한다
int getPriority                                            쓰레드의 우선순위를 반환한다

public static final int MAX_PRIORITY = 10       // 최대 우선순위
public static final int NORM_PRIORITY = 10    // 보통 우선순위
public static final int MIN_PRIORITY = 10       // 최소 우선순위
package com.bjy;

public class PriorityThread extends Thread {

	public PriorityThread() {
		
	}
	
	public void run() {
		int i =0;
		System.out.println(this.getName() + "[우선권:" + this.getPriority() + "] 시작");
		
		
		while(i<5000) {
			i++;
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				System.out.println(e.getMessage());
			}
		}
		System.out.println(this.getName() + "[우선권:" + this.getPriority() + "] 종료");
		
	} // end of run()

}
package com.bjy;

public class MainClass {

	public static void main(String[] args) {
		
		PriorityThread p1 = new PriorityThread();
		PriorityThread p2 = new PriorityThread();
		PriorityThread p3 = new PriorityThread();
		
		p1.setPriority(Thread.MAX_PRIORITY);
		p2.setPriority(Thread.NORM_PRIORITY);
		p3.setPriority(Thread.MIN_PRIORITY);
		
		p1.start();
		p2.start();
		p3.start();
		
	}

}

결과를 보면 우선순위가 가장 높은 p1(Thread-0)이 제일 먼저 끝나고 그 후 p2, p3이 순차적으로 끝나는 것을 볼 수 있다.

 

 

 

 

스레드의 동기화

멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기에 서로 영향을 주게 된다.

만일 계좌 입금 1000원하는 A스레드, 출금 3000원하는 B스레드가 있다고 가정할 때 초기 계좌 금액인 10000원에서 A스레드 진행 후 B 스레드 진행시 동기화를 하지 않게 되면 A스레드가 끝나기 전에 B스레드가 먼저 끝날 수도 있다. 그러면 계좌 금액은 10,000 + 1,000 - 3,000 = 8,000원이 아닌 10,000 - 3,000 = 7,000원으로 되기에 오류가 발생한다.

 

그래서 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막은 것을 '쓰레드의 동기화' 라고 한다.

 

스레드 비동기화 예시 

package com.bjy;

public class Bank {
	private int money = 10000; // 처음 잔액 초기값
	
	public Bank() {
		
	}
	
	/** 입금 처리 메서드 **/
	public void saveMoney(int save) {
		int m = this.getMoney();		// 추가 코드로 수정함으로써 변수에 직접 접근이 아닌 메서드 통한 간접 접근 이뤄짐
		try {
			Thread.sleep(3000);			// 입금 처리하는데 시간이 걸린다고 가정했을 때 3초동안 sleep
		} catch (InterruptedException e) {
			System.out.println(e.getMessage());
		}
		
		this.setMoney(m+save);
	}
	
	/** 출금 처리 메서드 **/
	public void minusMoney(int minus) {
		int m = this.getMoney();
		try {
			Thread.sleep(200);			// 출금 처리하는데 시간이 걸린다고 가정했을 때  3초동안 sleep
		} catch (InterruptedException e) {
			System.out.println(e.getMessage());
		}
		
		this.setMoney(m- minus); 
	}

	public int getMoney() {
		return money;
	}
	
	private void setMoney(int money) {
		this.money = money;
	}
	
}
package com.bjy;
/** 입금하는 메서드를 호출하는 스레드**/
public class Me extends Thread{
	
	public Me() {
		
	}
	
	public void run() {
		MainClass.bnk.saveMoney(3000);
		System.out.println("saveMoney(3000) : " + MainClass.bnk.getMoney());
	}

}
package com.bjy;
/** 출금하는 메서드를 호출하는 스레드**/
public class Wife extends Thread{

	public Wife() {
		
	}
	
	public void run() {
		MainClass.bnk.minusMoney(1000);
		System.out.println("minusMoney(1000) : " + MainClass.bnk.getMoney());
	}

}
package com.bjy;

public class MainClass {
	public static Bank bnk = new Bank();
	
	public static void main(String[] args) {
		
		System.out.println("현잔액 : " + bnk.getMoney());
		
		Me m = new Me();		// 입금 처리 시간 : 3초	
		Wife w = new Wife();	// 출금 처리 시간 : 0.2초
		
		m.start();
		
		try {
			Thread.sleep(200);
		}catch(InterruptedException e) {
			System.out.println(e.getMessage());
		}
		
		w.start();
		
		
	}

}

콘솔창 결과를 보면 계좌가 12000원이 아닌 13000원으로 되어있는데 동기화를 시키지 않아서 오류가 발생한 것이다. 이럴 때 Me, Wife 스레드의 동기화 혹은 Bank 클래스의 동기화 처리를 해주면 정상적으로 결과값이 출력하는데 다음은 동기화한 코드들이다.

 

package com.bjy;

public class Bank {
	private int money = 10000; // 처음 잔액 초기값
	
	public Bank() {
		
	}
	
	/** 입금 처리 메서드  : 구현부에 synchronized 동기화 방식 **/
	public void saveMoney(int save) {
		synchronized (this) {
			int m = this.getMoney();		// 추가 코드로 수정함으로써 변수에 직접 접근이 아닌 메서드 통한 간접 접근 이뤄짐
			try {
				Thread.sleep(3000);			// 입금 처리하는데 시간이 걸린다고 가정했을 때 3초동안 sleep
			} catch (InterruptedException e) {
				System.out.println(e.getMessage());
			}
			this.setMoney(m+save);
		}

	}
	
	/** 출금 처리 메서드 : 선언부에 synchronized 동기화 방식 **/
	public synchronized void minusMoney(int minus) {
		int m = this.getMoney();
		try {
			Thread.sleep(200);			// 출금 처리하는데 시간이 걸린다고 가정했을 때  3초동안 sleep
		} catch (InterruptedException e) {
			System.out.println(e.getMessage());
		}
		
		this.setMoney(m- minus); 
	}

	public int getMoney() {
		return money;
	}
	
	private void setMoney(int money) {
		this.money = money;
	}
	
}

Bank 클래스의 입금, 출금 메서드에 synchronized를 이용한 동기화 처리를 해주고 실행하면 정상적인 결과가 출력된다.

 

1. 메서드 전체를 임계 영역으로 지정
  public synchronized void calcSum() {
   }

2. 메서드내 특정 영역을 임계 영역으로 지정
  synchronized(객체의 참조변수) {
   }