Concurrent Programming 정리

Bruce Han·2023년 2월 12일
0

Java8-정리

목록 보기
13/20
post-thumbnail
post-custom-banner

이 포스팅의 코드 및 정보들은 강의를 들으며 정리한 내용을 토대로 작성한 것입니다.

Concurrent 소프트웨어

Concurrent 소프트웨어란?

동시에 여러 작업을 할 수 있는 소프트웨어

예시

  • 웹 브라우저로 유튜브를 보면서 키보드로 문서에 타이핑할 수 있다.
  • 녹화를 하면서 IntelliJ로 코딩하고 Word에 적어둔 문서를 보거나 수정할 수 있다.
  • 웹 브라우저로 강의를 보면서 SNS 프로그램으로 채팅할 수 있다.

Java에서 지원하는 Concurrent Programming

멀티 프로세싱(ProcessBuilder)

어떤 한 프로세스에서 다른 프로세스를 만드는 게 가능

멀티 스레드

Java 멀티스레드 프로그래밍

Thread 상속

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
}

concurrent

메인 메서드에서 어떤 어플리케이션이 동작하는지 스레드 이름을 확인하면 당연히 main이라는 이름이 출력된다.

메인 스레드에서 다른 스레드를 만들 수가 있다

public class Main {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        System.out.println("Main!");
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("[Thread] " + Thread.currentThread().getName());
        }
    }
}

MyThread라는 Thread 클래스를 상속받은 static class를 만들고 run()을 재정의하여 출력 한 문장 하나 하기로 한다.

main()에서는 MyThread 객체 하나를 만들어서 start()를 실행하고, 그 뒤에 Main!이라고 한 문장 출력한다.

순서상 myThread.start()가 실행되고 Main!이 출력되어야 할 텐데

concurrent1

Main!이 먼저 출력되고 그 다음 Thread를 출력하는 문장이 나온다. 무조건 이렇게 나오는 것은 아니고, 간혹 이렇게 실행되는 것이다.

concurrent2

이번에는 [Thread]로 시작하는 스레드 이름 출력하는 부분이 먼저 실행됐다.

이런 현상이 발생하는 이유는 스레드의 순서를 보장할 수 없기 때문이다.

Runnable 구현 또는 Lambda

Java 8 이전에 new Runnable()로 익명 클래스를 만들어서 재정의 하는 방식으로 만들었었다.

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("[Thread] " + Thread.currentThread().getName());
        }
    });
    ...
}

concurrent3

Java8 부터는 람다식을 활용할 수 있다.

public static void main(String[] args) {
    Thread thread = new Thread(() -> System.out.println("[Thread] " + Thread.currentThread().getName()));
    ...
}

이렇게 람다식을 통해서 한 줄로 줄일 수 있다.

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        System.out.println("[Thread] " + Thread.currentThread().getName())
    });
    ...
}

여러 줄 작업일 때는 중괄호를 활용하면 좋다.

Runnable이 functional interface로 바뀌었기 때문에 이런 게 가능하다.

스레드 주요 기능

현재 스레드 멈춰두기(sleep)

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("[Thread] " + Thread.currentThread().getName());
    });
	thread.start();

    System.out.println("[Main] " + Thread.currentThread().getName());
}

이렇게 Thread.sleep()을 통해서 대기시키면 다른 스레드한테 서버 리소스를 사용할 수 있는 우선권(우선순위)이 주어진다. 즉, 다른 스레드가 먼저 처리되는 것이다.

위 소스에서는 thread.start()를 하자마자 sleep() 상태에 들어가고, System.out.println("[Main] " + Thread.currentThread().getName())이 부분인 메인 스레드가 먼저 처리된다. 그러고 나서 자고 있던 스레드가 일어나서 처리될 것이다.

concurrent4

실행해보면 거의 [Main]을 출력하는 스레드(print문)가 먼저 실행될 거고, 그 다음 [Thread]부분이 출력될 것이다.

try~catch문에서 catch에 설정된 InterruptedException은 자는 동안에 누군가가 이 스레드를 깨우면 이 안에 들어온다. 즉, 깨우는 방법이 있다는 것이다.

다른 스레드 깨우기(interrupt)

interrupt는 다른 스레드를 깨우는 방법이다.

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        while(true) {
            System.out.println("[Thread] " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) { // 누군가 이 스레드를 깨우면 종료됨
                System.out.println("exit!");
                return;
            }
        }
    });
    ...
}

구현한 run()메서드 안에는 무한 루프를 돌면서 try~catch 문으로 sleep()을 실행하는 로직이 있다. 만약 interrupt가 발생하면 이 스레드는 종료되며, return문으로 인해 프로세스도 종료된다.

Runnable 인터페이스에 있는 run()은 다음 공식문서의 설명과 같다.

concurrent5

When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread.
The general contract of the method run is that it may take any action whatsoever.

Runnable 인터페이스를 구현하는 객체가 스레드를 만드는 데 사용되는 경우, 스레드를 시작하면 객체의 run()이 별도로 실행되는 스레드에서 호출된다.
메서드 실행의 일반적인 조건은 어떤 작업이든 수행할 수 있다는 것이다.

run()은 반환 타입이 void라 return을 굳이 안 해도 되지만 interrupt로 빠져나와야 하기에 추가한다.

		// try~catch 스레드 부분
        ...
		thread.start();

        System.out.println("[Main] " + Thread.currentThread().getName());
        Thread.sleep(3000L);
        thread.interrupt();
}

[Main] 스레드를 출력하는 부분 밑에는 sleep(3초)과 interrupt()를 추가했다. 참고로 interrupt()는 종료시키는 operation이 아니고 단지 깨우는(위 소스에서는 예외를 발생시키는) 메서드이다.

concurrent6

[Thread]가 세번 출력되고 마지막에는 interrupt()로 인해 exit!라는 출력과 함께 종료된다.

예외를 발생시키고 return문처럼 빠져나올 수 있는 장치가 없다면 스레드 출력은 계속될 것이다.
다른 스레드를 종료시킬 수 있는 방법은 있지만, 우리가 직접 해야 한다. 종료시키는 메서드가 따로 있는 것은 아니다.

다른 스레드 기다리기(join)

join()은 다른 스레드를 기다리는 것이다.

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println("[Thread] " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            // Runtime Exception - Unchecked Exception
            // 에러를 감싸서 예외 던지기, 로깅, 예외로 변환처리하면 적절
            throw new IllegalStateException(e);
        }
    });
    ...
}

이번에는 [Thread] 스레드 내에서 3초 정도 기다리는 로직을 구현했다. [Main] 스레드에서는 [Thread]를 기다리도록 join()을 활용할 것이다.

	// [Thread] 스레드 로직
	...
    thread.start();

    System.out.println("[Main] " + Thread.currentThread().getName());
    thread.join();
    System.out.println(thread + "is finished");
}

join()은 기다릴 스레드에다가 join()을 쓰면 된다. 그러면 먼저 [Main] 스레드의 이름이 출력될 거고 (혹은 [Thread] 스레드가 먼저 실행될 수 있음), 그 다음 [Thread] 스레드가 종료될 때까지 join()을 실행시킨 [Main] 스레드는 기다릴 것이다. 다 기다리면 그때 스레드 + "is finished" 라는 문장과 함께 스레드가 끝날 것이다.

concurrent7

join()이 실행된 상황에서 스레드가 계속 대기하고 있지만

	...
	System.out.println("[Main] " + Thread.currentThread().getName());
    try {
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(thread + "is finished");
}

이런 상황에서 누군가가 [Main] 스레드를 interrupt한다면 InterruptedException이 발생하게 될 것이다.
하지만 [Main] 스레드에서도 try~catch문으로 InterruptedException으로 예외 처리를 하게 해놨다면 스레드가 많이 복잡해질 것이다.
이런 스레드 로직이 더 많아진다면 관리하기 힘들어질 것이다.

그래서 Executors, Future, CompletableFuture을 정리하면서 많은 스레드를 어떻게 관리할 것인지 알아볼 것이다.

정리

  • Concurrent Software

    • 동시에 여러 작업을 할 수 있는 소프트웨어
    • 예) 웹 브라우저로 유튜브를 보면서 키보드로 문서에 타이핑할 수 있다.
    • 예) 녹화를 하면서 IDE로 프로그래밍하고 Word에 적어둔 문서를 보거나 수정할 수 있다.
  • Java에서 지원하는 Concurrent Programming

    • Multi processing (ProcessBuilder)
    • Multi Thread
  • Java MultiThread Programming

    • Thread 상속
    • Runnable 구현 / Lambda
  • 스레드 주요 기능

    • 현재 스레드 멈춰두기 (sleep) : 다른 스레드가 처리할 수 있도록 기회를 주지만 그렇다고 lock을 놔주진 않는다.
      • 잘못하면 데드락이 걸릴 수 있다.
    • 다른 스레드 깨우기 (interrupt) : 다른 스레드를 깨워서 interruptedException을 발생시킨다.
      • 그 에러가 발생했을 때 할 일은 프로그래밍하기 나름인데, 종료 시킬 수도 있고 계속 하던 일을 할 수도 있다.
    • 다른 스레드 기다리기 (join) : 다른 스레드가 끝날 때까지 기다린다.

Reference

profile
만 가지 발차기를 한 번씩 연습하는 사람은 두렵지 않다. 내가 두려워 하는 사람은 한 가지 발차기를 만 번씩 연습하는 사람이다. - Bruce Lee
post-custom-banner

0개의 댓글