본문 바로가기
프로그래밍 공부/Java

[lesson] Java 프로그래밍 언어 - 스레드(Thread)

by Luna_lua 2021. 8. 7.
반응형

지금까지 하나의 내용을 처리만 했는데 한번에 여러개를 처리하고 싶을땐 어떻게 해야할까요?

바로 오늘 배우는 스레드를 알게된다면 조금은 알게 될 것 같아요~

 

스레드를 배우기 전에 프로그램, 프로세스부터 알아볼까요?

 

<프로그램 vs 프로세스 vs 스레드>

프로그램

 - 아직 실행되지 않은 상태

 - 소스코드로 잘 짜여진 틀, 명령어의 집합

 

프로세스

 - 실행된 프로그램

 - 운영체제로부터 시스템 자원을 할당받는 작업의 단위

 - JAVA는 운영체제가 바로 실행시켜주지 않고 JVM에 의해 실행되기 때문에 JVM으로부터 시스템 자원을 할당받는다. 

    (자바는 이식성이 좋기 때문에 운영체제에 맞게 유동적으로 돌아갈 수 있도록 해놓음)

 

스레드 

 - 프로세스 처리 경로

 - 전적으로 JVM에 의해 스케줄링 된다.

 

 

 

<단일스레드와 멀티스레드>

- 단일 스레드
처리 경로를 한 개만 가지고 있기 때문에 직렬적이다.
동시에 많은 양을 처리하기 힘들기 때문에 상대적으로 비효율적이다.
하지만 하나의 작업에 문제가 발생하더라고 다른 작업에는 영향을 끼치지 않으며, 안정성이 보장된다. 

설계도 멀티 스레드에 비해 쉽다. 

main도 하나의 스레드!


- 멀티 스레드

하나의 프로세스를 동시에 처리하는 것처럼 보이지만 사실은 매우 짧은 단위로 분할해서 차례로 처리한다. 

여러개의 처리 경로를 가질 수 있도록 하며, 동시 작업이 가능해진다.
설계하기 굉장히 어려우며, 하나의 쓰레드 문제 발생 시 모든 스레드에 문제가 발생하게 된다.
java 웹 개발 시 사용되는 웹서버가 대표적인 멀티 쓰레드이다.
멀티 스레드로 설계했다면, 처리량 증가, 효율성 증가, 처리비용 감소의 장점이 있기 때문에
단점을 감수하고 설계하는 편이다.

 

<교착상태>

멀티스레드의 골칫덩어리 - 교착상태(DeadLock)
멀티 스레드 중 스레드 간에 대기 상태가 종료되지 않아서 무한정 대기만 하는 비정산적인 상태
교착생태인지를 판단했다면 전체 스레드를 깨워주거나 하나의 스레드를 종료시켜주면 교착상태가 해결된다.

 

< 멀티 스레드 구현 방법>

1. Thread 클래스 상속

2. Runnable 인터페이스 지정

★Point! 여기서의 핵심은 1. run메소드의 재정의,   2. 상속은 하나만 받을 수 있는데 클래스로 상속을 받을 것인지!

 

// Thread 클래스 상속하여 멀티스레드 구현

public class Thread1 extends Thread{
	String data;
	
	public Thread1() {;}
	public Thread1(String data) {
		super();
		this.data = data;
	}
	
    //Thread 클래스의 run메소드를 오버라이딩하여 기능을 넣어준다.
	@Override
	public void run() {

		for (int i = 0; i < 10; i++) {
			System.out.println(data);
	// sleep 시간동안 인터럽트가 일어난다. 
	// 그 인터럽트에 대한 오류를 처리하기 위해 try - catch문을 사용한다.
            try {
            // sleep(마이크로초) : 1초는 1000으로 기준!
				sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
}
// Runnable 인터페이스로 멀티스레드 구현

public class Thread2 implements Runnable{
	// Runnable의 run메소드를 재정의하여 기능 구현
    @Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName());
	// 스레드의 sleep상태에서 인터럽트가 일어나기 때문에
    // Thread로 구현했을때와같이 try - catch문을 사용하여 오류를 제어한다.
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

 

<동기화>

하나의 스레드가 실행 중일 때 다른 쓰레드가 동시에 같은 필드를 사용하지 못하게 되는것으로, 각 쓰레드를 제어할 수 있게 됩니다.

멀티스레드 구현 시 자원의 특정 부분만 하나씩 처리하고 싶은때 사용합니다. 선언방식은


1. synchronized(mutex){...}로 메소드처럼 만들어주어 일부 소스코드만 동기화를 걸어줍니다..

2. synchronized 키워드를 사용한 메소드에 걸어주어 메소드 전체에 동기화를 걸어줍니다. 

 

 

public class ATM implements Runnable{
	int money= 10000;
	
    // Runnable의 run메소드 오버라이딩
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			withdraw(1000);
			try {Thread.sleep(500);} catch (InterruptedException e) {;}
		}
	}
	
    // 일부 소스만 동기화시킬때 사용하는 메소드
	synchronized void use() {
		// 일부 소스를 동기화 할때 
		// 이부분(임계영역)에 코드를 작성하여 동기화를 해준다.
	}
	
    // 일반 메소드에 동기화 키워드를 걸어 동기화를 걸어준다.
	public synchronized void withdraw(int money) {
		synchronized (this) {
			this.money -= money;
		}
		System.out.println();
		System.out.println(Thread.currentThread().getName() + "이(가) " + money + "원 출금");
		System.out.println("현재 잔액 : " + this.money + "원");
	}
}

// Gs25에서 엄마와 아들이 동시에 하나의 계좌에서 돈을 꺼내기!
public class Gs25 {
	public static void main(String[] args) {
		// ATM 객체 생성
        ATM atm = new ATM();
		
        // ATM의 객체를 공유한다.

		Thread mom = new Thread(atm,"엄마");
		Thread sun = new Thread(atm,"아들");

        // thread의 run에서 사용하는 withdraw메소드에 싱크를 걸어주었기 때문에
        // start를 걸어도 동시에 사용되지 않고, 하나씩 사용하게 된다.
		mom.start();
		sun.start();
	}
}

 

 

 

 

+) thread는 main이 끝나도 따로 작동하기 때문에 안에서 끝나는 것이 아니면 프로그램이 끝날 때까지 꺼지지 않는다.

 

+) thread의 상태제어

1. join : 우선순위를 줄때는 join으로 실행한다.

Thread t1 = new Thread();
Thread t2 = new Thread();

t1.start();
t1.join();

t2.start();

// 라고 되어있으면 t1이 우선순위가 높기 때문에 t1을 먼저 실행완료 시키고 t2가 실행된다.

/*
* 주의사항! 
* 모두를 start를 걸거 join을 건다면 작동속도가 빠르기 때문에
* join이 걸리기 전 thread가 완료 될수있다.
*
* 그러므로 join이 필요하다면 
* thread start()를 사용후 바로 join()걸어주고 다른 스레드 작업을 해야한다.
*/
// 입력의 예시는 1 3 2 순으로 입력함을 알고 코드를 보시기 바랍니다.

// 스레드 기본 기능 선언
public class ThreadTask implements Runnable{
	모든 TreadTask의 타입은 cnt를 공유한다.
    public static int cnt;
	
    // 기본생성자
    // ;를 삽입한 이유는 일부러 비워 놓았다는 의미 (개발자들끼리 알아보기 위한 신호!)
	public ThreadTask() {;}

	public void printFirtst(Runnable first) {
		first.run();
	}

	public void printSecond(Runnable second) {
		second.run();
	}

	public void printThird(Runnable thrid) {
		thrid.run();
	}
	
    // switch는 thread의 이름에 따라 작동 방법을 정한다.
    // 들어온 순서의 thread이름은 1, 3, 2순서이고, cnt는 모두 공유하는 static 타입으로
    // 1번 스레드 first, 2번 스레드 third, 3번 스레드 second라고 출력된다.
	
    // run메소드 오버라이딩
    @Override
	public void run() {
		String name = Thread.currentThread().getName();
		switch(name) {
		case "1":
        	// Runnable 인터페이스 람다식 사용
        	// printFirst(Runnable Type)에서 Runnable은 @FunctionalInterface이므로
            // 하나의 추상메소드(run메소드)만 가지고 있으므로 람다식 사용이 가능하다.
            // 고로 () -> ~~~ 이것으로 runnable의 run메소드를 오버라이딩 할수 있다.
			printFirtst(()->System.out.println(++cnt +"번 쓰레드 first"));
			break;
		case "2":
			printSecond(()->System.out.println(++cnt +"번 쓰레드 second"));
			break;
		case "3":
			printThird(()->System.out.println(++cnt +"번 쓰레드 third"));
			break;
		}
	}
}





// 실행 메소드
import java.util.Scanner;

public class ThreadMain {
	public static void main(String[] args) {
//		
		Scanner sc = new Scanner(System.in);
		int[] arInput = new int[3];
		
		Thread[] arThread = new Thread[3];
		ThreadTask tt = new ThreadTask();
		
		for (int i = 0; i < arThread.length; i++) {
			arThread[i] = new Thread(tt);
		}
		
        // 몇번스레드를 먼저 실행시킬지 순서를 정해준다.
        // 예) 1 3 2
		System.out.println("입력 : ");
		for (int i = 0; i < arInput.length; i++) {
			arInput[i]= sc.nextInt();
            // 순서를 정해주고, 그 순서에 이름을 입력한 순서의 이름으로 지정해준다.
            // arThread[0]의 이름은 1
            // arThread[1]의 이름은 3
            // arThread[2]의 이름은 2
			arThread[i].setName(arInput[i] + "");
		}
		
		for (int i = 0; i < arThread.length; i++) {
        	// 이름이 1인 스레드, 3인 스레드, 2인 스레드 순으로 작동한다.
			arThread[i].start();
            // 시작 순서대로 join을 걸어준다.
            // 동시에 start를 해도 join이 걸려있기 때문에
            // 시작 순서대로 스레드가 작동한다.
			try {arThread[i].join();} catch (InterruptedException e) {;	}
		}
	}
}

 

2. wait : 스레드를 Runnable 상태에서 waiting 상태로 변경

※ Point! 모든 스레드가 wait걸리면 작동에 문제가 생기기 때문에 문제가 생긴다. (교착상태)

-> notify/notifyAll로 스레드를 모두 깨워준다.

 

3. notify / notifyAll : (특정/모든) 스레드를 깨운다

( 바로 실행이 아닌 실행중인 스레드가 있을수 있기 때문에 blocked 상태로 만들어주고 현재 작동중인 스레드가 끝나면 runnable상태로 변경된다.)

 

 

 

+) 스레드 종료 방법

1. 필드에 boolean타입의 변수를 선언하고 run안에 있는 반복문에 해당 변수가 true일때 break하도록 설계 
2. sleep()을 사용한다면 일시정지한 상태에서 InterruptedException을 발생시켜줌으로써 예외처리로 종료
    ※ 스레드객체.interrupt()를 사용하면 일시정지 상태에서 예외 발생
3. wait(), join을 사용한 로직이라면, sleep()을 사용한 후 종료를 진행해야하고,
   만약 위의 메소드를 사용하지 않는 로직이라면, sleep()을 사용하지 않아도 종료가 가능하다.
   ※ 스레드객체.interrypt()를 사용하면 Thread.interrupted()의 상태가 true로 변경된다.

반응형