티스토리 뷰

Java

자바 쓰레드(Thread)

토마's 2018. 2. 11. 12:20

안녕하세요. 오늘은 자바에서 쓰레드에 대한 내용을 포스팅 해보려고 합니다. 간단한 개념 및 예제를 통해서 진행하도록 하겠습니다.


개념?


프로세스란 간단히 말해서 '실행 중인 프로그램'입니다. 프로그램을 실행하면 OS으로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 됩니다. 프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리 등의 자원, 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드입니다. 모든 프로세스에는 최소한 한 개 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티 쓰레드 프로세스'라고 합니다.

만약에 하나의 프로그램에서 작업을 분할하여 여러개의 작업을 동시에 수행하고 싶을 경우에는 멀티 쓰레드를 사용하면 됩니다. 즉, 이해하기 쉽게 쓰레드를 프로세스라는 작업공간(공장)에서 작업을 처리하는 일꾼(worker)로 생각하시면 됩니다.


멀티 쓰레드의 장점


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

- 자원을 보다 효율적으로 사용할 수 있다.

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

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


그럼 간단한 예제를 통해서 쓰레드에 대해서 알아볼껀데, 쓰레드를 구현하는 방법에는 1. Thread 클래스를 상속 하는 방법과 2. Runnable 인터페이스를 구현 하는 방법으로 나눠집니다.



1. Thread 클래스를 상속


package test;

public class ThreadTest extends Thread {
	
	String name;
	
	public ThreadTest(String name) {
		System.out.println(getName() + "쓰레드가 생성되었습니다.");
		this.name = name;
	}
	
	public void run() {
		for(int i = 0; i < 10; i++) {
			System.out.println(getName() + " (" + name + ")");
			try {
				sleep(100);
			} catch (InterruptedException ie) {
				// TODO: handle exception
				ie.printStackTrace();
			}
		}
	}
	

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ThreadTest tt1 = new ThreadTest("thread_test1");
		ThreadTest tt2 = new ThreadTest("thread_test2");
		ThreadTest tt3 = new ThreadTest("thread_test3");
		
		tt1.start();
		tt2.start();
		tt3.start();
	}

}


다음과 같은 코드를 짜고 main 메소드를 통해서 만든 쓰레드를 테스트 해본 결과 아래와 같은 결과를 확인할 수 있습니다. 먼저 위의 코드에서 run() 메소드가 보입니다. 이 쓰레드를 실행하기 위해서는 run() 메소드가 반드시 필요합니다. run() 메소드 안에 실행하고자 하는 작업의 내용을 적어주시면 됩니다. 또한, sleep을 사용해서 0.1초 간격으로 작업이 수행되도록 설정했습니다. 그리고 try catch 문을 사용해서 작업중에 InterruptedException 예외가 발생하면 printStackTrace를 통해 에러가 발생한 원인을 잡아주며 단계별로 에러를 출력하게 됩니다. 위의 코드에서 세개의 쓰레드를 만들어 테스트 해보면 예를 들어, 첫번째 작업이 실행중이다가 CPU의 유휴 시간이 있을 경우에 다른 쓰레드의 작업을 수행하면서 자원을 효율적으로 사용할 수 있습니다.



main 메소드 안을 보면, ThreadTest 객체 tt1, tt2, tt3가 만들어지고 start 메소드를 사용해 각각에 대해 쓰레드를 실행시킵니다.(쓰레드는 run 메소드가 종료되면 알아서 소멸됩니다.) 그 결과, 쓰레드가 0부터 시작하여 2까지 순차적으로 실행되는게 아닌, 실행 순서가 정해져 있지 않은 비동기적인 실행을 보이고 있습니다.


 Thread.State 값

 설명 

 NEW 

 시작되지 않은 상태 

 RUNNABLE 

 실행 가능 상태  

 WAITING 

 대기 상태 

 TIMED_WAITING 

 쓰레드가 특정 시간 동안 대기 상태

 BLOCKED 

 쓰레드가 잠겨 있어 풀리기를 기다리는 상태 

 TERMINATED 

 쓰레드가 종료된 상태 


사실, 쓰레드가 동시에 수행하는 것처럼 보이지만 하나의 쓰레드만 CPU의 할당을 받아 실행되어집니다. start 메소드로 인해 tt1, tt2, tt3 객체들은 실행 가능한 상태가 되었고 JVM에서 쓰레드의 상태를 실행 가능한 상태와 실행 상태로 번갈아 가며 실행하여 동시에 수행되는 것과 같은 효과를 보이고 있습니다.


2. Runnable 인터페이스를 구현


package test;

public class ThreadTest2 implements Runnable {
	
	String name;

	public ThreadTest2(String name) {
		// TODO Auto-generated constructor stub
		System.out.println(name + " 쓰레드가 생성되었습니다.");
		this.name = name;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + " (" + name + ")");
			
			try {
				Thread.sleep(100);
			} catch (InterruptedException ie) {
				// TODO: handle exception
				ie.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ThreadTest2 tt1 = new ThreadTest2("test1");
		ThreadTest2 tt2 = new ThreadTest2("test2");
		ThreadTest2 tt3 = new ThreadTest2("test3");
		
		Thread tr1 = new Thread(tt1);
		Thread tr2 = new Thread(tt2);
		Thread tr3 = new Thread(tt3);
		
		tr1.start();
		tr2.start();
		tr3.start();
	}

}


우선, 위 코드를 실행하면 아래와 같은 결과를 확인할 수 있습니다.



위 코드가 첫 번째 코드와 다른 점은 Runnable 인터페이스를 구현한다는 점과 Runnable 인터페이스는 Thread라는 클래스를 상속 받지 못하므로 Thread 메소드를 이용하지 못하니 직접 Thread 클래스를 사용해서 sleep 메소드와 getName 메소드를 사용합니다. 그리고 ThreadTest2라는 객체를 만들었으나 직접 start 메소드를 사용할 수 없어서 Thread 객체를 생성하고 파라미터로 만들어진 ThreadTest2 객체를 넣어줍니다. 유의할 점은 Thread 클래스가 사용방법이 간단하긴 하나 Thread 클래스를 상속하면 다른 클래스를 상속할 수 없다는 것을 명심하면 됩니다.



Thread 우선순위


쓰레드는 우선순위에 따라서 실행을 우선 시 합니다. 즉, 우선 순위가 높은 쓰레드는 우선 순위가 낮은 쓰레드보다 많이 실행된다는 것을 의미합니다. 만약 동일한 우선순위를 가지고 있다고 하면 CPU의 할당 시간을 분배 후 실행하게 됩니다. 가장 높은 우선 순위는 10, 기본 우선 순위는 5, 가장 낮은 우선 순위는 1로 보통 쓰레드의 우선 순위는 기본 우선 순위인 5입니다. 

직접 쓰레드의 우선순위를 변경하고 싶다면, setPriority 메소드를 사용해서 변경해주시면 됩니다. 이것 또한 간단한 예제를 통해 확인해보도록 하겠습니다.


package test;

public class ThreadPriority extends Thread {
	
	String name;
	
	public ThreadPriority(String name) {
		// TODO Auto-generated constructor stub
		this.name = name;
	}
	
	public void run() {
		for(int i = 0; i < 10; i++) {
			System.out.println(name + " 우선 순위 : " + getPriority());
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ThreadPriority tp1 = new ThreadPriority("test1");
		ThreadPriority tp2 = new ThreadPriority("test2");
		ThreadPriority tp3 = new ThreadPriority("test3");
		
		tp1.setPriority(Thread.MIN_PRIORITY);	// 우선 순위 낮음
		tp2.setPriority(Thread.NORM_PRIORITY);	// 우선 순위 중간
		tp3.setPriority(Thread.MAX_PRIORITY);	// 우선 순위 높음
		
		tp1.start();
		tp2.start();
		tp3.start();
	}

}

결과를 확인해보시면 아래와 비슷한 결과를 확인할 수 있습니다. 위의 코드와 같이 말고도 직접 1부터 10까지 숫자를 주어서 우선순위를 정할 수 도 있습니다.




쓰레드의 동기화


실제로 멀티 쓰레드 프로그래밍에서는 2개 이상의 쓰레드가 실행되면서 데이터를 서로 공유합니다. 이 때, 데이터가 동시에 갱신(update) 된다면 문제가 발생하게 됩니다. 예를 들어, A와 B를 포함한 여러 쓰레드가 존재한다고 가정하고, A쓰레드가 작업을 수행하다가 중요한 값을 갱신하고 있었습니다 그런데 갑자기 실행 순서가 B로 바껴서 B가 새로운 작업을 다시 수행하면서 값을 갱신하다가 다른 쓰레드에게 권한이 넘어가면 데이터의 손실이 발생하여 심각한 문제를 불러올 것입니다. 따라서 아래 예제를 통해서 쓰레드의 동기화에 대해 알아보겠습니다.


ClassNumber 객체

public class ClassNumber {

	int num = 0;
	
	public void addNum() {
		num++;
	}
	
	public int getNum() {
		return num;
	}
}


Test 쓰레드

package test;

public class ThreadSync extends Thread {
	
	ClassNumber classNumber;
	
	public ThreadSync(ClassNumber cn) {
		// TODO Auto-generated constructor stub
		classNumber = cn;
	}
	
	public void run() {
		for(int i = 0; i < 5000; i++) {
			classNumber.addNum();
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ClassNumber number = new ClassNumber(); // 데이터 공유를 위한 클래스 생성
		ThreadSync ts1 = new ThreadSync(number);
		ThreadSync ts2 = new ThreadSync(number);
		ThreadSync ts3 = new ThreadSync(number);
		
		ts1.start();
		ts2.start();
		ts3.start();	
		
		try {
			ts1.join(); // ts1 쓰레드 수행후 ts2를 수행한다.
			// join() : 다른 쓰레드가ㅏ 하는 일이 마무리 될때까지 기다린다.
			ts2.join();
			ts3.join();
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		System.out.println(number.getNum());
	}

}

우선 위의 코드의 결과를 확인하시면 아래와 비슷한 결과를 확인할 수 있습니다. 3개의 쓰레드에 의해 15000이 출력되어야 되는 결과가 아닌 다른 결과가 나왔습니다. 그 이유로는 addNum 메소드에서 num++을 수행하다가 실행 권한이 다른 쓰레드로 넘어가서 동시에 이 문장을 실행합니다. 이러한 문제를 해결하기 위해서는 둘 이상의 쓰레드가 동시에 실행되지 않도록 설정하면 되고 그 방법은 동기화(synchronized) 키워드를 사용하면 됩니다.



수정된 ClassNumber 객체1

public class ClassNumber {

	int num = 0;
	
	public synchronized void addNum() {  // 동기화 메소드, 동시 실행 방지
		num++;
	}
	
	public int getNum() {
		return num;
	}
}


수정후 결과를 확인해 보시면 아래와 같이 정상적으로 15000이 출력되는 것을 확인할 수 있습니다.



동기화 메소드 방법은 메소드 내에서 수행할 내용이 많을 경우에는 쓰레드의 효율이 떨어질 수 있으며, 코드 일부를 동기화 대상으로 두기 위해 '동기화 객체'를 사용할 수 있으며 아래와 같이 사용하면 됩니다. 


수정된 ClassNumber 객체2

public class ClassNumber {

	int num = 0;
	
	public void addNum() {
           synchronized(this) {  // 이 부분만 동기화 영역으로 설정
		     num++;
           }
	}
	
	public int getNum() {
		return num;
	}
}


이것으로 기본적인 자바 쓰레드에 대한 개념 및 예제에 대한 포스팅을 마치도록 하겠습니다.