[JAVA] 쓰레드 ( Thread ) ①

DongGyu Jung·2022년 3월 26일
0

자바(JAVA)

목록 보기
44/60
post-thumbnail

🏃‍♂️ 들어가기 앞서..

본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕

*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.



🧶 프로세스 - 쓰레드

  • 프로세스 (Process) : 실행 중인 프로그램 ▶ " 자원(resources) " & " 쓰레드(thread) " 로 구성
    * 자원 → 메모리, CPU, 각종 devices

  • 쓰레드 (Thread) : 프로세스 내에서 실제 작업 수행
    * 모든 프로세스는 " 최소한 하나의 쓰레드 "를 가진다.

다시 말해
프로세스는 쓰레드가 일하는 환경으로서 쓰레드가 소속되어 있는 프로그램이라고 생각하면 된다.

또한 앞서 설명했듯
모든 프로세스들은 최소한 하나의 쓰레드를 존재하고

둘 이상의 쓰레드도 가질 수 있는데
이것을
" 멀티 쓰레드 프로세스(multi-threaded process) " 라고 한다.

싱글 쓰레드 프로세스 : (자원) + 쓰레드
멀티 쓰레드 프로세스 : (자원) + 쓰레드 + 쓰레드 + 쓰레드 ...


"왜 작업을 할 때, 쓰레드를 여러 개 사용할까?
그냥 여러 프로세스를 실행시키면 되지 않을까?"

라는 의문이 생길 수 있다.

하지만
하나의 새로운 프로세스를 생성하는 것보다
하나의 새로운 쓰레드를 생성하는 것이
적은 비용이 든다.
(ex. 한 명만 일할 수 있는 '공장'을 하나 더 짓는 비용 vs 한 공장에 인부만 더 고용하는 비용)


🎡 "멀티 쓰레딩" 장단점

"도스(DOS)" 와 같은 운영체제의 경우엔 " 한번에 한 가지 작업만 " 가능한데
"윈도우(Windows)"와 같은 운영체제는 멀티태스킹이 가능한 운영체제로 동시에 여러 작업을 수행할 수있다.


메신저를 살펴보면
쌍방간의 소통이 이루어지는 프로그램인데
만약 한번에 한 작업만 가능한 싱글 쓰레드라면 불가능할 것이다.

이처럼
"채팅" + "다운로드" + "음성통화" 등의 작업을 동시에 할 수 있는 이유가
바로 " 멀티 쓰레드 "이다.

또한
여러 사용자들에게 서비스를 제공하는 서버 프로그램의 경우도
싱글 쓰레드로 서버 프로그램을 구동한다면
"사용자의 요청" 마다 "새로운 프로세스"를 생성해야하는데

만약 1천 만명의 사용자가 동시에 요청을 한다면....?
그만 얘기하자

이렇기 때문에 프로세스 생성보다 쓰레드 생성하는 것이 훨씬 다방면으로 효율적이다.

😍 장점

  • CPU 사용률 향상
  • 효율적 자원 활용
  • 사용자 요청 응답성 향상
  • 작업 분리 & 간결한 코드

하지만 이런 말이 있다.

" 사공이 많으면 배가 산으로 간다 "

멀티쓰레드 프로세스는
《여러 쓰레드》가 같은 프로세스 내에서
자원을 공유하면서 작업을 하기 때문에

쓰레드들 간의 " 동기화 / 교착 상태 / 기아 " 문제들을 주의해야한다.

😥 단점 (주의점)

  • 동기화 ( synchronization )
  • 교착 상태 ( dead-lock ) 발생
  • 각 쓰레드의 효율적 실행 배분 을 위한 설계 중요
  • 교착 상태 (dead-lock)
    : 둘 이상의 작업이 " 서로 상대방 작업이 끝나기만 기다리고 있어서 서로 다음 단계로 진행하는 못하는 " 상태
    ▶ 서로 무한 대기 상태로 빠지게 된다.
    [컴활 1급] 준비할 때 공부했던 내용인데... 교착 상태에 빠지는 4가지 조건이 있다.
    【"상호배제" / "점유대기" / "비선점" / "순환대기"】

  • 기아 (Starvation) : 특정 프로세스의 " 우선순위가 낮아서 " 원하는 자원을 계~속 할당 못 받는 상태


🧨 구현 & 실행

※ 구현

쓰레드를 구현하는 방법에는 2가지 방법이 있다.
【 "Thread 클래스 상속" / "Runnable 인터페이스 구현"】

어떠한 방법을 선택해도 별반 차이는 없지만
이전에 배웠듯 상속은 다중 상속이 불가능하다.
그래서 다른 클래스를 상속받을 수가 없어 비교적 제한적이기 때문에

일반적으로 《Runnable 인터페이스를 구현하는 방법》을 사용한다.

또 해당 방법이
재사용성 (reusabillity)이 높고
코드의 일관성 (consistency)가 높은
보다 더 객체지향적인 방법이니

왠만하면 인터페이스 구현하는 방법을 사용하자.

1. Thread 클래스 상속

class MyThread1 extends Thread {
	/* Thread 클래스의 run() "오버라이딩" */
    public void run() {
    	// 작업내용 
    }
}

2. Runnable 인터페이스 구현

class MyThread2 implements Runnable {
	/* Runnable 인터페이스의 run() "구현" */
    public void run() {
    	// 작업내용 
    }
}

/* 
실제 Runnable 인터페이스는 
오로지 추상 메서드 "run()"만 정의 되어 있는 인터페이스다.
*/
public interface Runnable {
	public abstract void run();
}

※ 실행

단, 두 방법의 차이점은
Thread 객체를 생성하는 부분에 있다.

Runnable 인터페이스를 구현한 경우,
Thread 클래스를 상속받은 경우와 다르게

Thread 클래스 생성자를 통해 Thread를 생성하는데
이 때,
매개변수로 run()메서드를 구현한 외부 Runnable 객체를 입력해주어야 한다.
( run()외부로부터 받아오는 방식 _ Thread를 상속 안받고 오버라이딩도 안되어있기 떄문에 외부로부터 )

앞서 배웠었던 sort( Comparator c )의 경우와 유사하다.

// Thread 클래스 상속
MyTread1 t1 = new MyThread1() ;

// Runnable 인터페이스 구현 쓰레드 
// Thread t2 = new Thread(new MyThread2()) ;
Runnable r = new MyThread2();
Thread t2 = new Thread(r) ;


/* 실행 : start() */
t1.start();
t2.start();

==================================================
/* 활용 */
public class Practice1 {
	public static void main(String args[]) {
		Thread1 t1 = new Thread1();
		
		Runnable r = new Thread2();
		Thread t2 = new Thread(r);
		
		t1.start();
		t2.start();
	}
}

class Thread1 extends Thread {
	public void run( ) {
		for(int i = 0; i<5 ; i++) {
			System.out.println(getName()); // 이미 Thread 클래스를 상속받기 때문에 바로 getName() 메서드 사용 가능 _ this. 생략
            System.out.println(0);
		}
	}
}

class Thread2 implements Runnable {
	public void run( ) {
		for(int i = 0; i<5 ; i++) {
			// this 사용 불가 -> Thread.currentThread()
			System.out.println(Thread.currentThread().getName()); // Thread 클래스의 상속을 안받았기 때문에 Thread클래스의 currantThread() 메서드를 통해 연재 쓰레드의 정보를 받아와야함.
            System.out.println(1);
		}
	}
}

/*
매번 달라짐 : 2개의 쓰레드가 동시에 각자의 작업을 수행하는 것
<<출력>>
Thread-0
0
Thread-1
1
Thread-1
1
Thread-1
1
Thread-1
1
Thread-1
1
Thread-0
0
Thread-0
0
Thread-0
0
Thread-0
0

*/

위 코드를 만약
서로 다른 쓰레드를 두 개 이상 만들어서 실행되는 것이 아닌
싱글 쓰레드를 사용한다면 어떻게 될까

public class Practice1 {
	public static void main(String args[]) {
    	for(int i = 0; i<5 ; i++) {
			System.out.println(0);
		}
		for(int i = 0; i<5 ; i++) {
			System.out.println(1);
		}
	}
}

/*
한 번에 하나씩
순서대로 처리되는 것을 볼 수 있음
0
0
0
0
0
1
1
1
1
1
*/
  • static Thread cuurentThread() : 현재 실행 중인 쓰레드의 참조 반환
  • String getName() : 쓰레드 이름 반환

🚩 start()

" 실행 " <<< " 실행 가능한 상태로 변환 "

실행 순서를 결정하는 것은 "OS 스케쥴러"가 한다.

당연하겠지만
쓰레드는 생성했다고 자동으로 실행되는 것이 아니다.

생성한 쓰레드를 실행시키는 메서드가
start()이다.

단일 쓰레드라면
곧바로 실행되겠지만

만약
멀티 쓰레드 프로세스라면
실행대기 상태로 있다가 " 자신의 차례가 되면 실행 "된다.


또한
" start() 메서드는 ' 일회성 '인 점을 유의해야 한다. "

즉,
하나의 쓰레드 》에 대해
단 한번의 start() 》호출 가능하다는 것이다.

만약 한 번 실행되었던 쓰레드의 작업을
한 번 더 실행하고 싶다면

다시 새로운 객체 생성start() 재실행이 필요하다.
( 하나의 쓰레드에 대해 start()메서드를 두 번 이상 호출 → "IllegalThreadStateException" 예외 발생

❓ 그냥 run() 실행하면 안되나...?

결론부터 말하자면 안.된.다.!

필자만 생긴 의문일 수는 있으나

"""
Thread가 수행할 작업 내용이 담긴 겂은 run()메서드이고
굳이 start()를 실행하지 않고 바로 run()을 실행시키면
내부 코드가 바로 실행되지 않을까?

"""

라는 의문이 생겼다.

불가능한 것은 아니다.

그저 쓰레드고 뭐고
단순히 run()메서드를 실행하는 것 뿐이다.

이렇게 run()만 실행하게 되면 쓰레드 객체를 굳이 만들어 사용할 필요가 없다.


사실 Thread를 사용하는데에 있어 중요한 키워드는
" 호출 스택 (call stack) "인데

일단 가장 우선적으로
[ main 메서드 ]에서 시작을 할텐데

호출 스택 _ call stack
main

이 상태에서 start()를 실행하게 되면
새로운 호출 스택이 생성된다.

호출 스택 _ call stacknew 호출 스택
start
main

모든 쓰레드들은 자신만의 독립적 수행을 위해 독립적인 호출 스택을 필요로 한다.

→ 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출 스택이 생성되고
종료 되면 그 호출 스택은 사라진다.

실행된 쓰레드의 run()메서드가 실행된다.

호출 스택 _ call stacknew 호출 스택
mainrun

이렇게 독립적인 공간에서 자신의 작업을 수행한 후,
작업이 종료되면 해당 쓰레드의 호출 스택은 사라지게 된다.

호출 스택 _ call stacknew 호출 스택
main

호출 스택 _ call stack
main

여기서 만약 쓰레드가 여러 개라면

호출 스택 _ call stackThread 1 : 호출 스택Thread 2 : 호출 스택
(run 내부 작업 3)
start : Thread 2(run 내부 작업 2)(run 내부 작업 2)
start : Thread 1(run 내부 작업 1)(run 내부 작업 1)
mainrunrun

호출 스택 _ call stackThread 1 : 호출 스택Thread 2 : 호출 스택
(run 내부 작업 3)
(run 내부 작업 2)(run 내부 작업 2)
(run 내부 작업 1)(run 내부 작업 1)
mainrunrun

OS 스케줄러가 정한 순서에 따라
번갈아가면서 실행되게 된다.

0개의 댓글