본문 바로가기

개발/Java

[Java] 프로세스와 쓰레드 ( Java에서 쓰레드 구현 / 싱글 쓰레드 / 멀티 쓰레드 / 데몬 쓰레드 / 사용자 쓰레드 )

CS 공부하면서 프로세스와 쓰레드에 대한 이론적인 내용들을 학습했었다. 이번엔 실제로 자바에서 쓰레드가 어떻게 사용되는지 코드로 정리해보았다.

 

 

 

프로세스와 쓰레드

 

프로세스 : 프로그램 (운영체제로부터 자원을 할당받는 작업의 단위)

쓰레드 : 작업을 수행하는 일꾼 (프로세스가 할당받은 자원을 이용하는 실행의 단위)

 

 

 

프로세스 구조

Code + 메모리 영역( stack/Heap) + 데이터

 

1. Code(바이트 코드) : Java main 메소드와 같은 코드가 있는 공간

2. 데이터 : 전역 변수, 정적 변수(static), 배열 등 초기화된 데이터 저장 공간

3. Memory (동적) 

1) Stack : 지역변수, 매개변수 리턴 변수를 저장하는 공간

2) Heap : 프로그램이 동적으로 필요한 변수를 저장하는 공간 (new(), mallock())

 

 

 

쓰레드

프로세스 안에는 여러 쓰레드(일꾼)이 있다. 쓰레드들은 자신만의 독립적인 Stack영역을 보유하고 있고, 메모리 영역에서 Heap영역과 데이터, 코드 영역은 공유하여 사용한다.

 

 

 

자바에서 프로세스와 쓰레드

자바를 실행하면 JVM 프로세스가 시작된다. 자바에서는 Main 쓰레드(main() 메서드의 쓰레드)가 가장먼저 JVM에 의해시작되고, Main쓰레드가 종료되면, JVM도 같이 종료된다.

 

 

 

 

 

Java에서 쓰레드 구현 - 1. Thread 클래스

 

Java에서 제공하는 Thread 클래스를 상속받아 쓰레드를 구현할 수 있다. 아래의 코드는 Thread 클래스를 사용해 Thread를 사용하는 방법이다.

public class TestThread extends Thread {
	@Override
	public void run() { //run 메서드에 작성된 코드가 쓰레드가 수행할 작업
		// 쓰레드 수행작업
	}
}

... 
//Main문

TestThread thread = new TestThread(); // 쓰레드 생성
thread.start() // 쓰레드 실행

 

 

 

 

Java에서 쓰레드 구현 - 2. Runnable 인터페이스

 

Java에서 제공하는 Runnable 인터페이스를 사용하여도 쓰레드를 구현할 수 있다.

public class TestRunnable implements Runnable {
	@Override
	public void run() {
				// 쓰레드 수행작업 
	}
}

...
//Main
Runnable run = new TestRunnable();
Thread thread = new Thread(run); // 쓰레드 생성

thread.start(); // 쓰레드 실행

 

Runnable 인터페이스를 사용하는 방식이 Thread 클래스를 사용하는 것보다 조금 더 복잡하긴 하지만, 인터페이스는 다중상속이 가능하다는 장점때문에 확장성면에서 이점을 가지고 있다.

 

 

 

 

Java에서 쓰레드 구현 - 3. Runnable 인터페이스 + 람다식

 

Runnable 인터페이스에 람다식을 사용하여 쓰레드를 구현할 수도 있다. 이 방법이 실제로 가장 많이 쓰이는 방식이다.

//3번 람다식 (실제로 가장 많이 쓰이는 방식)
Runnable task = () -> {//정의한 작업을 task에 넣는다
   //여기에 쓰레드에서 수행할 작업 정의
   int sum = 0;
   for (int i = 0; i < 50; i++) {
        sum += i;
        System.out.println(sum);
    }
    System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
    };
    
    
Thread thread1 = new Thread(task); //Thread객체를 생성해서 task 넣는다.
thread1.setName("thread1"); //쓰레드 이름 설정

thread1.start(); //쓰레드 실행

 

아래의 모든 쓰레드 예시 코드들에서는 Runnable 인터페이스에 람다식을 사용하는 방식으로 쓰레드를 구현했다.

 

 

 

 

 

 

 

싱글 쓰레드

프로세스 안에서 하나의 쓰레드만 실행되는 것을 말한다. 자바의 경우엔 main() 메서드만 실행시켰을 때 싱글 쓰레드로 실행이 된다.

 

main 메서드에서 실행되고 있는 싱글 쓰레드 확인 예시

public class Main {
    public static void main(String[] args) {

        System.out.println("실행되고 있는 쓰레드 이름 " + Thread.currentThread().getName()); //Main

    }
}

 

 

실행 결과

싱글 쓰레드 실행 결과

 

 

 

 

 

 

 

멀티 쓰레드

프로세스 안에서 여러 개의 쓰레드가 실행되는 것을 멀티 쓰레드라고 한다. Java 프로그램은 메인 쓰레드 외에 다른 작업 쓰레드들을 생성하여 여러 개의 실행 흐름을 만들 수 있다.

 

여러 쓰레드를 통해 여러 개의 작업을 동시에 할 수 있어서 성능이 좋아지지만, 동기화의 문제가 발생할 수 있고 교착 상태( 둘 이상의 쓰레드가 서로의 자원을 원하는 상태가 되었을 때 서로 작업이 종료되기만을 기다리며 작업을 더 이상 진행하지 못하게 되는 상태)가 발생할 수 있다.

 

 

 

멀티 쓰레드 예시

public class Main {
    public static void main(String[] args) {

        //1st thread
        Runnable task = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.print("$");
            }
        };

        //2nd thread
        Runnable task2 = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.print("*");
            }
        };


        Thread thread1 = new Thread(task); //thread1 생성, task 할당
        thread1.setName("thread1");
        Thread thread2 = new Thread(task2); //thread2 생성, task2 할당
        thread2.setName("thread2");

        thread1.start(); //thread1 실행
        thread2.start(); //thread2 실행
        //결과를 보면 순서가 보장되어있지않다는 걸 알 수 있다.
        //걸리는 시간이나, 동작을 예측할 수가 없다 실행할 때마다 다르게 동작되기떄문(가변적)
    }
}

 

Runnable인터페이스에 람다식을 사용하여 쓰레드가 수행할 작업을 작성해주었다.

 

 

 

실행결과

멀티 쓰레드 실행 결과

 

결과를 보면 thread1이 먼저 실행($)되었지만, thread2의 작업(*)이 먼저 끝난 것을 확인 할 수 있다. 순서가 보장되어있지 않고, 실행할 때마다 다르게 동작된다는 것을 확인 할 수 있다.

 

 

 

 

 

 

 

데몬 쓰레드

보이지 않는 곳(background)에서 실행되는 낮은 우선순위를 가진 쓰레드를 말한다. 대표적인 예로 메모리 영역을 정리해주는 가비지 컬렉터가 있다. 데몬 쓰레드는 우선순위가 낮고 다른 쓰레드가 모두 종료되면 강제 종료 당한다.

 

public class Main {
    public static void main(String[] args) {
        Runnable demon = () -> {
            for (int i = 0; i < 1000000; i++) { //demon 쓰레드 task
                System.out.println("demon");
            }
        };

        Thread thread = new Thread(demon);
        thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨

        thread.start();

        for (int i = 0; i < 100; i++) { //main 쓰레드 task
            System.out.println("task");
        }
    }
}

//main 쓰레드의 task(100번 출력)가 먼저 종료되기때문에 demon 쓰레드는 1000000번 수행되기 전에 종료됨.

 

 

 

 

 

 

 

사용자 쓰레드

보이는 곳(foreground)에서 실행되는 높은 우선순위를 가진 쓰레드를 말한다. 프로그램 기능을 담당하며 대표적인 사용자 쓰레드로는 메인 쓰레드가 있다. 위의 코드 중 싱글, 멀티 쓰레드에서 만들었던 쓰레드들이 다 사용자 쓰레드이다. JVM 은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시켜 버린다.