Practice makes perfect

[JAVA] Thread 본문

빅데이터/JAVA

[JAVA] Thread

kerpect 2020. 5. 19. 22:36

Thread 란?

사용자가 작성한 코드로서, JVM에 의해 스케줄링되어 실행되는 단위입니다.

쉽게 설명해서 하나의 프로세스 안에서 2가지 이상의 일을 수행하는 것과 같은 것과 같은 효과를 주는 것을 의미합니다.

 

Java Thread

- 자바 가상 기계(JVM)에 의해 스케쥴되는 실행 단위의 코드 블럭입니다.

- 스레드의 생명 주기는 JVM에 의해 관리됩니다.

 

 JVM멀티스레드의 관계

- Thread 는 운영체제와 관련있는 기능으로 java 이외에 thread를 구현해주는 프로그램이 없습니다.

- 하나의 JVM은 하나의 자바 응용프로그램만 실행합니다.

- 자바 응용프로그램이 시작될 때 JVM이 함께 실행합니다.

- 자바 응용프로그램이 종료하면 JVM도 함께 종료합니다.

- 하나의 응용프로그램은 하나 이상의 스레드로 구성 가성합니다.

 

-- Thread 클래스 작성(Thread 클래스 상속. 새 클래스 작성)
class TimerThread extends Thread { 


-- Thread 코드 작성
run() 메소드 오버라이딩
	@Override
	public void run() { // run() 오버라이딩
		

-- Thread 객체 생성
TimerThread th = new TimerThread(); 

-- Thread 시작(start() 메소드 호출)
th.start();




run() 메소드 오버라이딩

- run() 메소드를 Thread 코드라고 부릅니다.

- run() 메소드에서 Thread 실행 시작합니다.

- run(); 을 통해서 main 2개인것과 같은 효과를 가지게 해주며,  main과 동시 수행합니다.

 

start() 메소드 호출

- Thread 작동 시작하고  JVM에 의해 스케줄되기 시작합니다.

- start(); 호출하러 가자마자 바로 복귀를 시켜서 다음 명령어를 수행하고, 운영체제에게 명령수행하기 전 준비하도록 요청합니다.

- Thread  수행하기 전 메모리들을 운영체제에게 요청해서 thead의 메모리를 할당받아 스레드의 run을 호출하게끔 구현이 되어져있습니다. main은 main 대로 수행하고, start로 바로 복귀 시켜 두개가 함께 수행하도록 합니다.

 

 

 

 

class SumThead extends Thread{

	String threadName; 
	int start, end;
	
	SumThead(String threadName, int start, int end){
		
		this.threadName = threadName;
		this.start = start;
		this.end = end;
	}
	
	@Override
	public void run() { 
		int sum = 0;
		
		for(int i = start ; i <= end; i++) {
			sum+=i;
			System.out.println(threadName);
		}
		System.out.printf("%s => %d ~ %d 까지의 합 : %d \n", threadName, start, end, sum);
	}
}
public class ThreadUnderstand {
	public static void main(String[] args) { 
		SumThead  thread1 = new SumThead ("쓰레드1", 1, 10); // main 안에서 new 하는 컨셉은 변함없다. 
		// 1~10까지의 합
		
		SumThead  thread2 = new SumThead ("쓰레드2", 11, 20);
		// 11~20까지의 합 
		
		int sum = 0;
		
		thread1.start(); 
		thread2.start(); 
		
		for(int i = 1 ; i <= 50 ; i++) {
			sum += i;
			System.out.println("main");
		}
		System.out.println("main() 메서드 실행 => 1~50까지의 합 : " + sum);
		System.out.println("프로그램 종료.");
	}
}

결과값 : main, thread1, thread2 함께 수행하는 효과를 주지만 출력되는 순서는 일정하게 나타나지 않습니다. 이것은 

현재 운영되는 프로그램에 따라서 다른 결과를 나타나게 됩니다. 결과의 순서는 운영체제의 알고리즘만 알 수 있습니다. 
결과값을 통해서 3개의 작업이 골고루 수행되는 효과를 보여줄 수 있습니다.

 

 

자료형을 선언했을 때, 그 자료형을 thread 로 상속받고 싶을 때 다른 class 를 상속받았을 때 사용법)

=Runnable 사용

class Sum {
	int num; 
	
	Sum(){num=0;}
	
	public void addNum(int num) {this. num += num;}
	public int getNum() {return num;}
}

class AddThread extends Sum implements Runnable{    
	
	AddThread(int start , int end){
		this.start = start;
		this.end = end;
	}

	int start, end; 
	                                                          
	@Override                                                          
	public void run() {   
		for(int i = start; i <= end ; i++){
			addNum(i);
		}System.out.printf("%d ~ %d 의 총 합은 : %d  \n", start, end, num);
	}   
}

public class RunnalbleThread {
	public static void main(String[] args) {
		
		 AddThread at1 = new  AddThread(0,50); // thread 기능이 탑재 되어 있지 않다. 
		 // runnable을 상속받는  AddThread을 사용하여 run을 사용한다. 
		 
		 AddThread at2 = new  AddThread(51,100);
		 
		Thread thread1 = new Thread(at1); // run 을 수행시킬 코드를 만들어줘야 한다. 
		Thread thread2 = new Thread(at2); 
		
		thread1.start();
		thread2.start();
		
		try {
			thread1.join();
			thread2.join();
		} catch (InterruptedException e) {  
			e.printStackTrace();
		} 
		
		// join() method는 try~each 문으로 감싸줘야하며, 
        // join() method는 run(); method가 수행될때까지 기다린다. 
		
		System.out.println("0~100까지의 합 : " +  (at1.getNum()+at2.getNum()));

 

※ thread기능을 탑재 시키기 위한 유일한 방법

- Sum 을 상속 받아서 thread를 상속하지 못할 때 implements Runnable 인터페이스 사용합니다.
- Runnable 안에 run(); 추상method 하나 들어가있습니다. 또한 thread와는 상관이 없으며 단지 run method 만

  오버라이딩 되어 있는 것입니다.

 

- join() method는 run(); method가 수행될때까지 기다렸다가 수행합니다.

 

 

여기서 위의 코드의 문제점!

(at1.getNum()+at2.getNum())의 값이 계속 다르게 나타납니다.


그 이유는 thread1, thread2 , main 함께 시작하기 때문에 컴퓨터의 상태에 따라서 언제 실행될지 알 수 없습니다.
thread1, thread2이 구동되기 전에 main은 벌써

System.out.println("0~100까지의 합 : " + (at1.getNum()+at2.getNum())); 실행할수도 때문에 결과가 항상 다르게 출력합니다.
thread1, thread2가 구동이 되고 나서 System.out.println("0~100까지의 합 : " + (at1.getNum()+at2.getNum())); 실행 되어야지 올바른 결과값이 나타납니다.  

 

위의 코드를 해결 우선순위를 넣어서 코드를 만들어보겠습니다.

 

 

우선순위)

class MessageSendingThread extends Thread{

    String message; 
    
    // 우선순위 부여(절대적인 것이 아닌 가중치정도록 생각하자.)
    MessageSendingThread(String message, int prio){
    	
    	this.message = message;
    	setPriority(prio); // 값을 다시 setting 할 수 있게 만들어준다. 
    }
	
	@Override
	public void run() {  // main 2개 효과 
		for(int i = 0; i < 1000; i++) {
			System.out.printf("%s(%d) \n" , message, getPriority()); 
		}	
	}
}

public class PriorityTest {
	public static void main(String[] args) {
		
		// 우선순위 
		MessageSendingThread tr1 = new MessageSendingThread ("First", Thread.MAX_PRIORITY); 
        // ("First", 10);
		MessageSendingThread tr2 = new MessageSendingThread ("Second", Thread.NORM_PRIORITY); 
        // ("Second", 5);
		MessageSendingThread tr3 = new MessageSendingThread ("Third", Thread.MIN_PRIORITY); 
        // ("Third", 1);
		
	    tr1.start();
	    tr2.start();
	    tr3.start();	
	}

}

- setPriority(prio) 을 생성자에 초기화 시켜줍으로 run(); method안에 숫자를 넣어 우선순위 대로 출력하도록 만들어줍니다.  

 

- 우선순위가 부여되었다고 해서 Thread의 특성상 결과에 있어서 정확하게 순서에 맞춰서 출력되지는 않습니다. 각각 1000가지 씩 수행되어야 하는데, 우선순위가 낮다고 해서 무조건 늦게 나오는 것이 아니라는 점을 염두해두면 좋겠습니다.

 

동기화)

: 하나의 thread 의 동작이 완전히 진행된 이후 순서를 넘기는 것을 의미합니다. (synchronized)

class Sum{
    int num; 
    
	Sum(){num=0;}
    
	public synchronized void addNum(int num) {this. num += num;} 
	public int getNum() {return num;}                                                                                         
}                                                                        
                                                                           
class AdderThread extends Thread{

	Sum sumInst ;
	int start, end;
	
	AdderThread(Sum sum, int s , int e){
		this.sumInst = sum;
		this.start = s;
		this.end = e; 
	}
	
	@Override
	public void run() {
		for(int i =start ; i<= end ; i++) {
			sumInst.addNum(i);
		}
	}
}

public class ThreadHeapMultiAccess {
	public static void main(String[] args) {
		Sum sum = new Sum();
		
		AdderThread at1 = new AdderThread(sum , 1 , 5000); // sum의 주소값이 저장
		AdderThread at2 = new AdderThread(sum , 5001 , 10000); // sum의 주소값이 저장
		
		at1.start();
		at2.start();
		
		try {
			at1.join();
			at2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

 

최종적으로 업데이트가 균일하게 진행되면 문제가 없지만 그렇지 못하게 때문에 나오는 문제점입니다.

위의 코드를 보면  at1 과 at2 가 같은 data를 가지고 수행하는데 Thread의 특성으로 인해서 올바른 값이 출력되지 않습니다. 이러한 문제점을 해결하기 위해서 동기화를 하나의 값이 정확히 수행되고 나서 다음 값이 진행되록 만들수 있습니다.

 

- 위와 같이 둘 이상의 thread 가 heap 영역하나의 data 를 바라보도록 접근해서 처리하게 되면 문제가 생기므로 동시화를 고려해야 합니다. 왜냐하면 순서가 왔다갔다 하면서 수행을 하게 되면 결과 값이 날아가버리기 때문입니다.

 

- synchronized 를 활용하여 동작이 완료되서 복귀 될 때까지 순서가 넘어가지 않도록 할 수 있습니다. 단, 제어권을 넘기지 않고 다 수행한 이후 제어권을 넘기도록 하면 출력 속도가 느려지기 때문에 꼭 필요한 경우에 사용하는 것을 권장합니다.