[TIL] JAVA - 쓰레드

배고픈메꾸리·2021년 2월 22일
0

SSAFY

목록 보기
13/22

Concurrent vs Parallel

Concurrent는 어떤 Job이 여러개 동시에 처리되는 개념
Parallel 은 하나의 Job을 쪼개서 여러 Sub-Job으로 나누고 동시에 처리해서 완성하는 개념


용어 정리

프로세스 : 개별적으로 동작하는 프로그램 ( = 프로그램이 실행된 것)
쓰레드 : 프로세스를 구성하는 독립적인 세부 단위
멀티 프로세스 : 여러 프로세스를 동시에 수행
멀티 쓰레드 : 한 프로세스에서 여러 개의 쓰레드가 동시에 수행

멀티 쓰레드 프로그래밍의 장/단점

멀티 쓰레드를 사용하는 이유는 크게 CPU 사용률 향상 , 응답성 향상 , 자원의 공유를 통한 효율성 증대를 들 수 있다.
하지만 컨텍스트 스위칭 과정에서 별도의 비용이 발생하거나 제어의 어려움은 쓰레드를 사용하면서 오는 단점이라고 볼 수 있다.


쓰레드 생성

쓰레드는 일반적으로
1)Runnable 인터페이스를 구현하는 방법과
2) Thread 클래스를 상속받는 2가지 방법
으로 생성할 수 있다.

1) Runnable 인터페이스의 구현

먼저 Runnable 인터페이스를 구현하는 방법부터 알아보자. Runnable에는 run() 메서드 하나가 존재하고 이 메서드를 오버라이딩 해서 필요한 내용을 작성한다. 일반적인 자바 프로세스의 출발점이 main인 것 처럼 스레드의 출발점은 run() 메서드 이다. (콜 스택의 최 하단에 위치한다.)

public interface Runnable
	public abstract void run();
}

하지만 Runnable을 구현한 것은 아직 쓰레드가 아니다. 두 번째 단계로 Runnable 객체를 Thread의 생성자에 인자로 넘겨줘서 Thread객체를 생성해야 쓰레드가 생성된다

MyThread t1 = new Thread(new Runnable(){
	public void run(){
    		System.out.println("Hello");
        }
});

2) Thread 클래스 상속

Thread 클래스는 Runnable을 구현하고 있다. 따라서 별도로 Runnable 객체를 파라미터로 넣을 필요 없이 Thread 클래스만 가지고 쓰레드를 만들 수 있다.

public class Thread implements Runnable{
...
}

이 때 Thread 클래스의 run() 메서드를 오버라이딩 해서 로직을 구현해야 한다.

class myThread extends Thread{
	public void run(){
    		System.out.println("Hello");
        }
}
Thread t = new MyThread();

Thread를 상속받는 경우 코드 작성은 쉬워지지만 단일 상속의 제약에 의해 다른 클래스를 상속받을 수 없다는 단점이 발생한다.


쓰레드 실행

쓰레드를 실행할 때는 run 메서드를 통해 호출하지 않는다. 쓰레드를 동작 시킬 때는 Thread 클래스에 선언된 start메서드를 사용한다.
run메서드는 쓰레드에서 수행할 작업을 정의하는 메서드이고
start 메서드 호출은 쓰레드의 메서드가 호출될 수 있도록 준비하는 과정이다. (실제로 호출하는 것은 JVM이다 마치 우리가 main메서드를 직접 실행시키지 않는 것과 같다.)

// 간단한 쓰레드 예시 , 결과는 항상 다르다
public class SimpleThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 30; i++) {
					System.out.print("-");
				}
			}

		});

		Thread t2 = new Thread() {
			public void run() {
				for (int i = 0; i < 30; i++) {
					System.out.print("@");
				}
			}
		};
		t1.start();
		t2.start();
		System.out.println("main is over");
	}
}

쓰레드와 메모리

[단계 1 ]

최초의 자바 어플리케이션이 구동되면 메인 쓰레드가 하나의 스택을 차지한다. 이 때 스택에서는 프로세스의 힙/공유 자원을 자유롭게 이용할 수 있다.

[단계 2]

메인 쓰레드가 t1 쓰레드의 start() 메서드를 호출하면 t1 쓰레드를 위한 별도의 스택 공간을 구성한다. 이 스택은 메인 쓰레드와 전혀 무관한 공간이다. 따라서 메인 쓰레드와 서로의 자원을 사용할 수는 없다. 하지만 힙/공유 자원은 두 개의 쓰레드가 모두 접근 가능하다.

[단계 3]

새로운 스택에서 run() 메서드가 실행되면서 기존의 메인 쓰레드와는 별도의 흐름이 생겨난다. 이후 두 쓰레드는 번갈아가며 작업을 수행하게 된다. 이 때 모든 쓰레드가 종료되어야만 JVM이 종료된다.

JVM은 최초에 메인 스레드인 메인 메서드를 찾아서 실행한다. 이 때 메인 메서드가 다른 쓰레드를 생성하면 경쟁을 하게 된다.

※오해하기 쉬운 것들※

1. 메인 메서드는 start 시킨 모든 쓰레드들이 종료된 후에 종료될까??

메인 메서드는 쓰레드들과 같이 경쟁하는 사이이다. 쓰레드들이 전부 종료되면 JVM의 실행이 종료된다.

2. 쓰레드 객체를 하나만 만들고 start 메서드를 두 번 호출하면 어떻게 될까?

오류가 발생한다.

3. 쓰레드안에서 또 다른 쓰레드를 실행 시킬 수 있을까?

가능하지만 공유 자원(변수 / 자료구조) 등에 대한 주의가 필요하다


쓰레드의 상태와 제어

쓰레드의 상태

enum상수설명
NEW쓰레드 객체가 생성된 후 아직 start()되지 않은 상태
RUNNABLEJVM 선택에 의해 실행 가능한 상태
BLOKED사용하려는 객체의 모니터 락이 풀릴 때까지 기다리는 상태
WAITINGsleep(), wait() ,join() 등에 의해 정해진 시간 없이 대기중인 상태
TIMED_WAITINGsleep(), wait() ,join() 등에 의해 정해진 시간 동안 대기중인 상태
TERMINATErun() 메서드의 종료로 소멸된 상태

  1. 최초 쓰레드 객체를 생성하면 new상태 이다. 이 상태의 객체는 타입만 Thread 일 뿐 쓰레드의 동작은 하지 않는다. 이 때 start() 메서드를 호출하면 Runnable 상태로 변경되고 쓰레드 스케줄러에 의해 선택되면 동작될 수 있다.

  2. JVM이 쓰레드의 run 메서드를 호출하면 쓰레드가 동작한다. run 메서드가 종료되면 쓰레드는 terminate 된다.

  3. 동작중인 쓰레드는 쓰레드의 sleep wait join 메서드가 호출되거나 I/O에 의한 블로킹이 발생할 경우 대기 풀로 이동한 뒤 waiting 혹은 timed_waiting 상태로 변경된다.

  4. 대기 상태의 쓰레드는 sleep join 시간 종료 notify interrupt I/O 종료 가 발생하는 순간 다시 Runnable 상태로 변경된다.

  5. 동작중인 쓰레드에서 yield 메서드가 호출되면 쓰레드는 동작을 멈춘다. 하지만 대기상태로 이동하지는 않고 Runnable 상태로 이동하게 된다.


쓰레드 동기화

쓰레드들이 공유하는 자원에 대한 관리는 필수적이다. 어떤 쓰레드가 자원에 대한 작업을 수행할 때 그 작업이 완료되기 전에 다른 쓰레드가 접근해서 다른 작업을 실행하게 되면 심각한 오류가 발생할 수 있다.

예를들면 A와 B가 10000원이 있는 계좌에서 작업을 한다고 가정하자. 쓰레드 A가 계좌에서 10000원을 출금하는 중에
쓰레드 B도 10000원을 출금하려고 한다. A가 아직 출금하지 않았기 때문에 A,B쓰레드 모두 계좌에 10000원이 있는 상태라고 전달 받았고, 이로 인해 A도 10000원을 출금하고 B도 10000원을 출금하는 돈복사버그가 발생한다.

이러한 문제를 해결하기 위해서는 A가 출금을 하는 중엔 B가 계좌에 접근하지 못하게 하는 방식이 필요할 것이다. 그리고 이러한 방식을 동기화(Synchronize) 라고 한다.

public void run(){
	synchronized(job){
	    ...
    	}
}

동기화를 통해서 하나의 메서드나 메서드 안의 특정 블럭을 지정하여 Lock 을 부여할 수 있다. Lock 을 부여받은 작업은 원자성을 갖게 되는데, 다른 작업이 Runnable 상태에 있다고 하더라도 해당 작업이 끝날 때 까지는 Running 상태에서 변경되지 않는다.


오후 실습 실행 코드

import java.util.Scanner;

public class 실험실 {
	public static void main(String[] args) {
//		// implements
//		MyThread2 mt2 = new MyThread2();
//		Thread t2 = new Thread(mt2);		
//		t2.start();

		// extends
		MyThread tt = new MyThread();
		tt.start();

		// 전부 쓰지마 !
		/*
		 * tt.stop(); // 쓰레드 강제 종료 : 정상적인 종료를 할 수 없다.
		 * tt.suspend(); // 쓰레드 일시 정지 :
		 * tt.resume(); //쓰레드 일시 정지한 것을 해제 = Runnable 상태로 변경
		 *
		 */
		Scanner sc = new Scanner(System.in);
		while (true) {
			int menu = sc.nextInt();
			if (menu == 0) { // 쓰레드 종료
				// tt.stop(); // 쓰레드 종료 호출 안됨 ( 비정상 종료 )
				tt.doing = false;
				break;

			} else if (menu == 1) {  // 쓰레드 일시정지
				tt.pause = true;
			}else if(menu == 2){  // 쓰레드 일시정지 해제
				tt.pause = false;
			}
		}

	}
}

class MyThread extends Thread {
	public boolean doing = true; // 완전 종료
	public boolean pause = false; // 일시 정지

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (doing) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) { // 인터럽트 발생하면 깨어남
				e.printStackTrace();
			}
			if (!pause) {

				System.out.println("안녕");
			}
		}
		System.out.println("쓰레드 종료");
	}
}

//class MyThread2 implements Runnable{
//
//	@Override
//	public void run() {
//		// TODO Auto-generated method stub
//		
//	}
//	
//}
profile
FE 개발자가 되자

0개의 댓글