Hayden's Archive

[자바/Java] 스레드 (Thread) 본문

Study/Java & Kotlin

[자바/Java] 스레드 (Thread)

_hayden 2020. 4. 23. 10:23

 프로세스(Process) 

- 쓰레드를 알려면 프로세스를 알아야 함.

- 프로세스(Process) = 독립적인 하나의 실행 파일. 

- 현재 실행중인 파일 = 현재 점유하고 있는 파일

- 예) AppTest.java 컴파일 해서 -> AppTest.class 실행 파일(이게 프로세스 파일)

Multi-Tasking 
- 예컨대 AppTest.class, 카카오톡, 다운로드 동시에 실행한다. 우리 눈에는 동시에 실행하는 것으로 보이지만 실제로는 TimeSliced 방식으로 돌아가는 것.

- 여러 개의 프로세스가 동시에 돈다 => 서로 다른 프로세스가 시간을 쪼개가면서 CPU를 쓴다.


 스레드(Thread) 

원서에서 스레드를 표현한 그림. 여기서 data = 프로세스의 데이터 

 

- 스레드 : 전문화된 작업 단위. 특정 일만 전담해서 하는 작업을 스레드로 만듦. 그런데 그런 것들이 여러 개 돌아감. → 따라서 스레드는 "프로세스 내에서" 진행되는 세부적인 "작업 단위".

Multi Tasking System
- Multi-Processing(여러 가지를 동시에 처리하는 것)
- Multi-Threading(스레드를 여러 개 돌리는 것)

 

 

 


- 프로세스와 스레드의 차이점 
: 프로세스들은 독립적인 작업. 완전히 다른 코드. / 스레드는 같은 프로세스 안에서 돌아가므로 자원(Resource)을 공유(Sharing)할 수 있다. ( 참고 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html )

동기화(Synchronized) 처리 : 자원을 공유하는 상태에서 동시에 달려들면 문제가 생길 수 있어서 그걸 잘 관리해주는 것. (관련 포스팅 : Vector와 ArrayList https://hayden-archive.tistory.com/77 ) - 아래 참고

데드락(DeadLock) : 동기화 처리를 했는데 문제가 발생한 것.( 참고 : https://terms.naver.com/entry.nhn?docId=797064&cid=42347&categoryId=42347

 


 스레드 클래스(Class Thread) 

java.lang - Classes - Thread
https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html

A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently. (스레드는 프로그램 안에서 실행 스레드이다. JVM은 응용프로그램이 동시에 실행되는 다양한 실행 스레드를 가지는 것을 허용한다.) 

Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. (모든 스레드는 우선순위를 가진다. 높은 우선순위를 가진 스레드는 낮은 우선순위를 가진 스레드보다 우선적으로 실행된다.)


우선순위(Priority)는 1부터 10까지 있음. (예를 들어 우선순위 1 => 가비지 컬렉터. 평소에는 1이지만 메모리가 꽉 찼을 때 우선순위 10이 되어서 실행됨)


자바 API에서 어떤 코드가 나와있으면 굉장히 중요한 코드. 한 번 볼 것! 

class PrimeThread extends Thread {
//PrimeThread 클래스가 Thread 클래스를 상속받음
	long minPrime;
	PrimeThread(long minPrime) {
		this.minPrime = minPrime;
	}

	public void run() { 
    // 스레드가 하는 일을 run 메소드에 정의하면 됨.  
    //내가 호출하는 게 아니라 내부적으로 호출되기 때문에 그대로 써야 함.
    // (리턴타입 변경x 인자값x 예외처리x)
		// compute primes larger than minPrime
		. . .
	}
}


The following code would then create a thread and start it running:

PrimeThread p = new PrimeThread(143); 
p.start(); 

 

★★★★★위 코드에 따르면 객체 생성하고 p.run()을 해야 동작할텐데 p.start()로 함.    
개발자는 start()로 호출. 이 때 스레드가 run하는 건 아니고 실행가능한 상태가 됨.

JVM의 Schedulerrun()을 호출해서 우선순위가 높은 스레드를 실행함. 개발자는 calling의 주체가 아님.

Class Diagram -> 클래스끼리의 관계
State Diagram -> 객체의 상태가 어떻게 전이되었는가
State Diagram 중에서 가장 유명한 게 State Diagram of the Thread. ==> Thread가 태어나서 죽을 때까지 Life cycle을 한눈에 볼 수 있음.

 

순서 :
1. 새로운 스레드가 만들어지고 개발자가 start()를 호출한다.
2. start() 하면 스레드가 큐스택 매커니즘( 참고 : https://blog.naver.com/minj2477/220691892859 )으로 빠지고 대기행렬에 서게 된다. (CPU를 점령(run)한 상태는 아니고 대기 중인 것.)
3. 내부적으로 JVM의 스케쥴러run()을 호출하고 우선순위가 높은 스레드가 실행된다.
4. 실행되던 스레드에 sleep()를 호출하면 해당 스레드의 CPU 점유권이 해지되고(Block) 다시 대기행렬에 줄을 서게 된다.
5. 스레드의 작업이 다 끝나면 스레드는 알아서 소멸된다.

- stop() 메소드 : 정상종료 안 하고 팍 꺼버리는 것. 어차피 스레드는 실행이 다 끝나면 알아서 소멸됨. API 문서를 보면 Deprecated( 뜻 : https://en.dict.naver.com/#/entry/enko/c4dbe94228834e2597a64ca4094f3f22 )라고 적혀있음. 굳이 쓰지 말라. 

- sleep() 메소드 : 실행중인 스레드에 sleep() 메소드를 건다. 인자값을 넣어주는데 sleep(1000)은 1초 동안 타임캡슐에 들어가 있는 것. 꼭 알아야 하는 메소드. API 보면 예외처리 하고 들어가야 함.( InterruptedException )

- currentThread() 메소드 : 현재 돌고 있는 스레드를 리턴함. static이라서 Thread.currentThread()로 쓸 수 있음.

- Thread.currentThread().getName() : 현재 돌고 있는 스레드의 이름을 가져옴

- getName()이 있다는 얘기는 setName()도 있다는 소리. setName()으로 스레드 이름 줄 수 있음. 아니면 생성자로 주입할 수 있다.

package thread.step1;
//Thread 클래스
public class ComeThread extends Thread {// 스레드 클래스 상속
	public ComeThread(String name) {
		super(name); // 스레드 클래스 생성자 호출.
	}
	
	// 스레드가 작동하는 부분
	// run 메소드는 개발자가 호출하는 게 아니라 내부적으로 호출되기 때문에 
	// 건드리지 말고 있는 그대로 써야 함.
	// -> 리턴타입 변경x 인자값x throw로 예외처리x
	// -> sleep는 예외 처리 해줘야 하는데 run 메소드 뒤에는 throw 못 붙임.
	public void run() { 
		int i = 0;
		while(true) {
			try {
				Thread.sleep(1000); // sleep는 static. 1초 동안 타임캡슐에 있음.
			
			}catch(InterruptedException e) {
				
			}
			String tname = Thread.currentThread().getName();
			// 현재 실행 중인 스레드의 이름을 가져옴
			System.out.println("CurrentThread :: "+tname+","+i);
			i++;
			if(i == 20) break;
		}
		
	}
}
package thread.step1.test;

import thread.step1.ComeThread;
import thread.step1.GoThread;

public class GoComeThreadTest {
	
	public static void main(String[] args) {
		//1. Thread 생성....
		GoThread go = new GoThread("GoThread");
		ComeThread come = new ComeThread("ComeThread");
		
		//2. start()를 호출
		go.start(); // Runnable한 상태 ---> 내부적으로 run(); ---> run(){}
		come.start(); // Runnable한 상태 ---> 내부적으로 run(); ---> run(){}	
	}
}

 

- run() 메소드 호출을 받은 메소드는 무조건 CPU 점유를 보장받는다(O) -> 시간이 조금 걸릴 뿐 무조건 running됨.(내부적인 우선순위는 알 수 없음)

- 스레드의 본질 : interrupted. 끼어드는 것. waiting하지 않고 끼어든다.


 Runnable 인터페이스(Interface Runnable) 

java.lang - Interfaces - Runnable
https://docs.oracle.com/javase/7/docs/api/java/lang/Runnable.html

- API를 보면 run() 메소드 하나밖에 없음. 근데 이건 JVM의 스케쥴러가 호출하는 것.

- start() 메소드가 없어서 개발자가 출발을 못 시킴. -> 방법 : Thread 클래스의 객체를 생성해서 생성자에 인자값을 넣고 Thread 클래스의 start() 메소드를 호출한다.

- Thread 클래스 API에 가서 Constructor Summary에서 생성자 인자값 확인

/*
 * 스레드를 만드는 방법
 * 1. extends Thread
 * 2. implements Runnable -> 시중의 책에서는 이 방법을 더 추천. 
 * 상속은 다중상속이 불가능해서 그럼. 하지만 크게 상관 없다
 */
package thread.step2;
//Thread 클래스
public class GoThread implements Runnable {
	/*public GoThread(String name) {
		super(name); 
	}*/ // 부모가 인터페이스므로 부모 생성자 호출 불가능.

	@Override
	public void run() {
		int i = 0;
		while(true) { 
			try {
				Thread.sleep(1000); 
			}catch(InterruptedException e) {
				
			}
			String tname = Thread.currentThread().getName();
			System.out.println("CurrentThread :: "+tname+","+i);
			i++;
			if(i == 20) break;
		}
	}
}
package thread.step2.test;

import thread.step2.ComeThread;
import thread.step2.GoThread;

public class GoComeThreadTest {
	public static void main(String[] args) {
		GoThread go2 = new GoThread();
		ComeThread come2 = new ComeThread();
		
		Thread tgo = new Thread(go2, "GoThread"); 
		//Runnable 상속받은 객체를 인자값으로 받으면 됨.
		// 스레드 클래스 API에서 생성자 참고. 
		Thread tcome = new Thread(come2, "ComeThread");
		
		tgo.start();
		tcome.start();
		
	}

}

 


 멀티 스레드 (Multi-thead) 

실행 클래스, 테스트 클래스 ==> 결론적으로 프로세스. 모든 프로세스에는 스레드가 1개 이상은 있어야 함. main 메소드스레드에 해당됨. 


메인 스레드프로세스를 돌리는 스레드. 메인 스레드는 작업 스레드가 아니고 전체적인 조율을 할 뿐. 메인 스레드 하나만 있으면 병렬 작업이 불가능함. 싱글 스레드 모델(Single Thread Model)

작업 스레드프로세스 내에서 특정한 일을 전담하는 기능을 한다. 메소드와 비슷.

/*
 * BeepPrintTest2 실행 클래스..... 하나의 프로세스
 * 이 안에 메인스레드 + 작업스레드 하나 더 추가
 * 1) 메인 스레드 : beep()를 5번 ---- 경고음 5번 	발생 
 * 2) 작업 스레드 : BeepPrintThread ---- 띵을 5번 출력
 * 
 */
package thread.step3.test;

import java.awt.Toolkit;

class BeepPrintThread extends Thread{
	public void run() {
	// 띵띵띵띵띵을 5번 출력하는 작업
		for(int i = 0; i < 5; i++) {
			System.out.println("띵~~~");
			try {
				Thread.sleep(500); // 0.5초로 재움.
			} catch (InterruptedException e) {

			}
		}
	}
}

public class BeepPrintTest2 {
	public static void main(String[] args) {
		BeepPrintThread beepT = new BeepPrintThread();
		beepT.start(); 
        // 여기를 기점으로 하나는 위의 run()을 실행하고 
        // 하나는 아래 코드를 실행함.
		// start()가 병렬적인 작업이 실행되는 시점.
		
		Toolkit tool = Toolkit.getDefaultToolkit(); // static 메소드임.
        
		// 경보음 5번 울리는 작업
		for(int i = 0; i < 5; i++) {
			tool.beep(); 
            //경보음을 울려줌. 
            // 근데 이것만 쓰면 너무 빨리 지나가서 5번으로 안 들리고 1번으로 들림.
			
			try {
				Thread.sleep(500); // 그래서 0.5초로 재움.
			} catch (InterruptedException e) {
				
			}
		}
	}

}
참고

Toolkit 클래스 API ( https://docs.oracle.com/javase/7/docs/api/java/awt/Toolkit.html )

Toolkit 설명 ( https://blog.naver.com/oraclejava31/150162165300 )

데몬 스레드 ( https://yoonemong.tistory.com/216 )

 


 스레드의 프로세스 자원 공유 

- 스레드는 같은 프로세스 안에서 돌아가므로 자원(Resource)을 공유(Sharing)할 수 있다.

- 아래 코드는 10부터 1까지 카운팅 작업이 들어가면서 동시에 숫자 입력을 받는데 ①카운팅이 끝나기 전에 숫자 입력을 하면 카운팅이 종료됨 ②카운팅이 끝나기 전에 숫자 입력을 하지 못하면 입력할 수 없음

package thread.step4.test;

import javax.swing.JOptionPane;
/*
 * 카운팅작업 - CountThread
 * 숫자입력작업 - InputThread
 * ::
 * 두 스레드 간의 Communication은 프로세스의 자원으로 해야 한다.
 */
class CountThread extends Thread{
	private InputThreadTest3 process;
	public CountThread(InputThreadTest3 process) {
		this.process = process;
	}

	public void run() {
		//2. 일종의 카운팅 작업...
		for(int i = 10; i > 0; i--) {
			if(process.inputCheck == true) break;
			System.out.println(i);
			try {
				Thread.sleep(600); // 0.6초
			}catch(InterruptedException e) {

			}
		}
		if(!process.inputCheck) {
			System.out.println("10초 경과되었습니다... 값 입력 시간 초과!!!");
			System.exit(0); // 프로그램 정상 종료
		}
	}
}

class InputThread extends Thread{
	private InputThreadTest3 process;
	public InputThread(InputThreadTest3 process) {
		this.process = process;
	}
	
	public void run() {
		//1. 데이터 입력 작업...
		String input = JOptionPane.showInputDialog("최종 로또 번호를 입력하세요..."); 
		//스캐너는 콘솔로 출력하는데 이건 폼에다가 입력할 수 있도록 만든 것.
		System.out.println("입력하신 숫자는 "+input+" 입니다.");
		process.inputCheck = true;
	}
}

// Process....
public class InputThreadTest3 {
	boolean inputCheck = false; 
	public static void main(String[] args) {
		InputThreadTest3 process = new InputThreadTest3();
		CountThread countT = new CountThread(process); 
		InputThread inputT = new InputThread(process);
		// 프로세스를 통으로 가져가게 함. 자원 공유를 위해. 
		// 자원을 해징하기 위해서(스레드가 프로세스를 가짐) 
		// 스레드가 프로세스를 가지고 놀게 함.
		
		countT.start();
		inputT.start();
		
	}

}

 동기화(Synchronized) 처리 


- 어떤 스레드가 작업하고 있는 동안에는 다른 스레드가 못 건드리게 해야 함. 하나의 스레드가 작업을 다 끝내기 전에는 다른 스레드가 작업의 제어권을 가져가지 못하도록 한다. -> 그러기 위해 동기화 처리를 해야 한다.

- 예컨대 티켓팅을 예로 들면 공유 데이터(ex : seat 필드)를 쉐어링할 때 아직 user1이 티켓팅 중인데 user2가 끼어들 수 있음. -> 이 때, 동기화 처리를 하게 되면 공유 자원에 자물쇠가 잠김. -> 먼저 실행된 스레드가 일을 다 끝나기 전까지 다른 스레드가 끼어들 수 없음.

- 동기화 처리는 클래스 전체에 걸지 않음(엄청나게 느려짐). 내가 필요한 기능에만 건다.

- 하나의 스레드가 작업을 다 마무리하기 전까지 다른 스레드가 치고 들어오지 못하게 하는 것동기화 처리이고 모니터 모델링 기법으로 설명됨.

 

package thread.step6;
/*
 * MegaBoxUser는 MegaBox에서 좌석을 예매하는 일을 전담하는 스레드라고 간주..
 * reserve() 라는 기능을 하나 작성... 예매 로직을 작성
 */
public class MegaBoxUser implements Runnable {
	private boolean seat = false; // 좌석 예매가 끝나면 true 할당
	@Override
	public void run() {
		try {
			reserve();
		}
		catch(InterruptedException e) {
			
		}
	}
	
	public synchronized void reserve() throws InterruptedException { 
		// 스레드가 작업하고 있는 영역에 동기화 처리
		// run 메소드가 아니므로 throw 할 수 있음.
		// 호출한 곳에 run 메소드이므로 run 메소드에서 try~catch로 예외처리 한 것.
		String tName = Thread.currentThread().getName(); // 현재 실행중인 스레드의 이름을 받아옴.
		System.out.println(tName+"님, 예매하러 오셨습니다..!!");
		
		if(seat==false) {//좌석이 비었다면..
			Thread.sleep(2000);//2초 동안 잠듦. 스레드에게 2초는 20년처럼 긴 시간.
			// 하지만 synchronized 했으므로 실행 중인 스레드에 다른 스레드가 끼어들 수 없다.
			System.out.println(tName+"님, 좌석 예매 성공");
			seat = true;
		}else {//좌석이 이미 완료되었다면...
			System.out.println(tName+"님, 해당 좌석은 이미 예매 완료된 좌석입니다.");
		}
	}

}
package thread.step6.test;

import thread.step6.MegaBoxUser;

public class MegaBoxUserProcess {
// 프로세스는 스레드 하나는 반드시 가지는데 그게 바로 메인 스레드
	public static void main(String[] args) {
		MegaBoxUser user = new MegaBoxUser();
		Thread t1 = new Thread(user, "김00");
		Thread t2 = new Thread(user, "이00");
		
		t1.start();
		t2.start();
	}

}