본문 바로가기
CS

[Java] 멀티 스레드 구현하기

by 벨롭 2024. 1. 28.

혼자서 공부하는 컴퓨터 자료구조 + 운영체제 책을 보고 있는데, 해당 책은 깃허브를 통해 심화 코드를 제공하고 있다. (https://github.com/kangtegong/self-learning-cs) 그런데 프로세스와 스레드 코드는 C++과 python로만 제공되고 있어서, 동일한 흐름을 가지고 java로 직접 구현을 해 보았다.

자바에서는 멀티 프로세싱을 직접적으로 지원하지 않고 있다. 도커 등을 이용해서 병렬 실행을 구현할 수는 있지만, 해당 글에서는 멀티 스레딩을 위주로 서술한다.

 

 

사전 학습

 

스레드와 멀티 스레드 개념이 낯설다면 아래 글을 먼저 읽고 오는 것을 추천한다.

2023.10.13 - [CS/운영체제] - [운영체제] 스레드와 멀티 스레드

 

[운영체제] 스레드와 멀티 스레드

스레드의 의미 예를 들어 게임에서 점프를 할 때 동시에 호잇! 하는 효과음을 낸다고 생각해보자. 프로세스의 진행 흐름이 한 가지라면 점프 코드를 실행한 다음에 효과음을 내고, 그 간격을 최

kyeong8139.tistory.com

 

 

 

 

 

 

코드로 스레드 만들기

 

파일명: Main.java

public class Main {
    public static void main(String[] args) {
        long pid = ProcessHandle.current().pid();
        System.out.println("process Id: " + pid);
    }
}

우선 'process id + PID' 형태로 출력해보자. 위 소스 코드를 실행하면 프로세스가 생성된다. 이 프로세스에 Thread 클래스를 상속받은 자식 클래스를 이용해 새로운 스레드를  만들 수 있다.

 

 

파일명: Foo.java

public class Foo extends Thread{
    @Override
    public void run() {
        long threadId = this.getId();
        long pid = ProcessHandle.current().pid();

        System.out.println("thread id:" + threadId);
        System.out.println("process id:" + pid);
    }
}

 

파일명 : Main.java

public class Main {
    public static void main(String[] args) {
        long pid = ProcessHandle.current().pid();
        System.out.println("process Id: " + pid);

        Foo thread1 = new Foo();
        thread1.start();
    }
}

 

 

실행 결과

process Id: 9051
thread id:13
process id:9051

 

 

참고로 스레드 ID를 출력하는 자바의 메서드는 Thread 클래스의 getId()이다.

 

주의할 점은, 위 소스 코드를 직접 실행하면 본 블로그의 실행결과와 프로세스 ID와 스레드 ID 숫자가 다를 수 있다는 점이다. 여기서 중요한 점은 프로세스 내에서 ID가 13인 스레드를 만들어 냈다는 부분이다.

 

 

 

 

 

동일한 작업을 하는 스레드 생성하기

 

 

파일명: Main.java

public class Main {
    public static void main(String[] args) {
        long pid = ProcessHandle.current().pid();
        System.out.println("process Id: " + pid);

        Foo thread1 = new Foo();
        Foo thread2 = new Foo();
        Foo thread3 = new Foo();

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

 

 

실행 결과

process Id: 9198
thread id:13
process id:9198
thread id:14
process id:9198
thread id:15
process id:9198

 

thread1, thread2, thread3라는 세 개의 스레드는 모두 각기 다른 스레드지만, 이들은 모두 동일한 프로세스 내에서 실행되고 있다. 그래서 스레드 ID는 모두 다르게 출력되고, 프로세스 ID는 모두 동일하게 출력된다.

 

 

 

 

각기 다른 작업을 하는 스레드 생성하기

 

public class Main {
    public static void main(String[] args) {
        long pid = ProcessHandle.current().pid();
        System.out.println("process Id: " + pid);

        Thread foo = new Thread() {
            @Override
            public void run() {
                System.out.println("This is foo");
            }
        };

        Thread bar = new Thread() {
            @Override
            public void run() {
                System.out.println("This is bar");
            }
        };

        Thread baz = new Thread() {
            @Override
            public void run() {
                System.out.println("This is baz");
            }
        };

        foo.start();
        bar.start();
        baz.start();
    }
}

 

 

 

실행 결과

process Id: 9598
This is foo
This is baz
This is bar

 

이처럼 하나의 프로세스 내에서 여러 개의 실행 흐름을 만들어낼 수도 있다. 편의를 위해 익명 클래스를 이용해서 구현하였다.

 

 

 

 

 

 

멀티스레딩에서 동기화가 중요한 이유

 

 

public class Main {
	
	static int num = 0;
	
	public static void main(String[] args) {
		
        Thread plus = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 1000000; i++) {
					num = num + 1;
				}
			}
		};
		
		Thread minus = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 1000000; i++) {
					num = num - 1;
				}
			}
		};
		
		plus.start();
		minus.start();
		
		System.out.println(num);
	}
}

 

 

 

Thread plus는 더하기 1을 100만번 실행하는 스레드이고, Thread minus는 빼기 1을 100만번 실행하는 메소드이다.

plus와 minus가 실행되면 결과가 어떻게 될까? 더하기 1과 빼기 1이 각각 100만번씩 실행되었으므로, 0이라고 생각할지도 모른다. 하지만 정작 실행해보면 0이 아닌 다른 값이 나오는 일이 빈번하다. 바로 NUM이라는 임계구역에 동시에 접근하는 일이 발생할 수 이기 때문이다.

 

 

 

 

우리가 보기에 num = num + 1;이라는 한 줄의 코드이지만, 컴퓨터에 더 가까운 어셈블리어 수준으로 작성하면 이정도가 될 것이다. 

LOAD R0, num              // num에 저장된 값을 R0 레지스터에 로드

ADD R0, R0, #1            // R0 레지스터에 있는 값을 1 증가시키고, R0에 결과값을 저장함

STORE R0, num           // R0 레지스터에 있는 값을 num에 저장함

 

 

 

 

그런데 Thread plus가 실행되고 있다고 해서 반드시 LOAD-ADD-STORE이 한 큐에 실행되는 것이 아니다. 예를 들어 plus의 LOAD, ADD 까지 실행된 후 minus의 LOAD, ADD, STORE가 실행되었다고 생각해보자.

 

plus에서는 num = 0에서 1을 더해 R0에 1이라는 값을 가지고 있는데, num에는 저장하지 못한채 minus가 실행될 수도 있다. 그러면 minus는 num = 0에서 1을 뺸 -1이라는 값을 R0에 저장하고 num에 -1을 저장한다. 그 상태에서 plus의 STORE가 실행되면 -1 위에 1을 덮어쓰게 된다. plus와 minus가 한 바퀴씩 돌았는데 결과값은 1이 되는 상황이 발생하는 것이다.

 

 

따라서 멀티스레드에서는 동기화를 통해 상호 배제를 구현하는 것이 필요하다.