ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 멀티 스레드 - 개념, 생성과 실행, 우선순위, 동기화 메소드(synchronized)
    개발/Java 2020. 12. 27. 19:45

    '이것이 자바다' 교재를 참고하였습니다.
    코드는 여기에

    멀티 스레드

    목차

    멀티 스레드 개념

    프로세스와 스레드

    프로세스란 운영체제에서 실행중인 하나의 애플리케이션을 의미한다. 사용자가 애플리케이션을 실행하면 운영체제는 실행에 필요한 메모리를 할당해주고 코드를 실행하는데 이것이 프로세스다.
    예를들어 구글 크롬을 실행시키면 하나의 프로세스를 생성한 것이고, 구글 크롬을 2개 띄우면 프로세스를 두개 생성한 것이다.

    멀티 태스킹이란 두가지 이상의 작업을 동시에 처리하는 것이다. 운영체제는 멀티 태스킹을 위해 자원을 적당히 할당하고 병렬로 실행시킨다.
    멀티 프로세스도 멀티 태스킹이지만, 한 프로세스 내에서 여러가지 작업을 처리할 수 있다. 멀티 스레드 방식이다.

    스레드는 사전적 의미로 한가닥의 실이라는 의미인데, 여기서 의미를 따와 한가지의 실행 흐름을 의미한다고 한다.
    따라서 멀티 스레드는 하나의 프로세스에 여러개의 실행 흐름이 생기는 것을 의미한다. 실제의 예를 들면 메신저안에서 채팅을 하며 동시에 파일을 보내느 것을 예로 들 수 있다.

    만약 엑셀과 파워포인트를 띄운 멀티 프로세스라면 둘 중 하나의 프로세스가 죽어도 다른 프로세스에는 영향이 없다.
    그러나 멀티 스레드에서는 채팅을 하며 파일을 보내다가 파일을 보내는 스레드가 죽으면 채팅 스레드에도 영향이 있다.

    그렇기 때문에 멀티스레드에서는 예외 처리가 아주 중요하다.

    멀티 스레드는 대용량 데이터 처리에서 데이터를 병렬로 처리해 처리 시간을 줄일 때, UI를 가진 애플리케이션에서 네트워크 통신을 할 때, 다수의 클라이언트 요청을 처리하는 서버를 개발할 때도 사용된다.

    메인 스레드

    main() 메소드가 실행하면서 시작된다. main()의 첫줄부터 아래로 순차적으로 실행하다가 return을 만나거나 main() 메소드의 마지막줄을 실행하면 종료된다.
    메인 스레드에서 다른 작업 스레드르를 만들어서 병렬로 작업을 수행할 수 있는데,
    이때 메인 스레드의 작업이 끝나도 다른 작업 스레드들이 끝나지 않았다면 프로세스는 종료되지 않는다.

     


    작업 스레드 생성과 실행

    멀티 스레드로 실행하는 애플리케이션을 만들 때, 몇 개의 작업을 병렬로 처리할지 결정하고 각 작업별로 스레드를 생성해야 한다. (메인 스레드에 추가로 몇 개인지.)
    자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요한데, 방법이 두가지가 있다.

    1. java.lang.Thread를 직접 객체화해서 생성하기
    2. Thread를 상속해서 하위 클래스로 만들어 생성하기

    1.java.lang.Thread를 직접 객체화해서 생성하기

    직접 객체화하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.
    Runnable은 인터페이스이기 때문에 구현이 필요하고, run()메소드만을 가지고 있다.
    Runnable은 무엇을 실행할지 나타내는 인터페이스이지, 스레드는 아니다. 그래서 반드시 스레드를 생성하고 그 매개값으로 주어야 한다.

    Thread thread = new Thread(new Runnable() { // new Thread()로 직접 Thread를 객체화 
        public void run() {
            // ....
        }
    };

    람다식으로 구현하면 Thread thread = new Thread(() -> { 구현 });

    BeepPrintExample1.java를 보면 삡 소리와 '띵' 출력을 동시에 하고 싶지만 순차적으로 진행되기 때문에 삡 소리가 다 끝나고 출력이 시작된다.

    for (int i = 0; i < 5; i++) {
        toolkit.beep();
        try {
            Thread.sleep(500);
        } catch (Exception e) {
        }
    }
    
    for (int i = 0; i < 5; i++) {
        System.out.println("띵");
        try {
            Thread.sleep(500);
        } catch (Exception e) {
    }

    toolkiet.beep();과 출력문을 한 블럭에 놓아도 동시는 아니고 타이밍이 약간 어긋난다. 어쨌든 순차적으로 실행되기 때문이다.

    동시에 작업을 수행하기 위해서 두 작업중 하나를 다른 스레드에서 실행시켜주어야 한다. 그래서 삡 소리를 내는 작업 스레드를 새로운 구현 클래스에 작성한다.(BeepTask.java)

    java.lang.Thread 클래스로부터 작업 스레드를 직접 생성하는 중이니 마찬가지로 Runnable 인터페이스의 구현 클래스를 작성하면 된다.

    또는 BeepPrintExample2.java에서 익명함수나 익명함수 람다식을 이용해서 구현해도 된다.의
    람다식으로 구현

    Thread thread3 = new Thread( () -> {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for (int i = 0; i < 5; i++) {
            try {
                toolkit.beep();
                Thread.sleep(500);
            } catch (Exception e) {
    
            }
        }
    });
    thread3.start();

    2. Thread 하위 클래스로부터 생성

    작업 스레드가 실행할 작업을 Runnable로 만들지 않고 Thread의 하위 클래스로 작업 스레드를 정의하여 작업 내용을 포함시킬 수도 있다.
    즉, Thread 클래스를 상속하고 run 메소드를 오버라이딩해서 스레드가 실행할 코드를 작성한다.

    Thread thread = new BeepThread(); // new Thread()로 직접 Thread를 객체화하지 않음. 상속받아 구현한 BeepThread()를 객체화함.
    thread.start();
    
    for (int i = 0; i < 5; i++) {
        System.out.println("띵");
        try {
            Thread.sleep(500);
        } catch (Exception e) {
    
        }
    }

    스레드의 이름

    스레드는 자신의 이름을 가지고 있는데 주로 디버깅시에 필요하다.
    쓰레드_인스턴스.setName() : 스레드 이름 지정
    쓰레드_인스턴스.getName() : 스레드 이름 가져오기
    Thread.currentThread() : 현재 해당 코드가 실행중인 스레드 객체 참조 얻기

    스레드 우선순위

    동시성(Concurrency) : 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질
    병렬성(Parallelism) : 멀티 작업을 위해 여러개의 코어가 멀티 스레드를 동시에 실행하는 성질

    동시성은 워낙 빨라서 병렬적으로 실행하는 것 처럼 보이지만, 하나의 실행 흐름에서 스레드를 번갈아가며 실행하는 것이다.

    스레드의 개수가 코어의 개수보다 많은 경우 어떤 순서에 의해 동시성으로 실행할 것인지 결정해야 하는데 이것을 스레드 스케줄링이라고 한다.
    만약 스레드의 개수가 코어의 개수보다 적은 경우에는 스케줄링이 큰 영향을 못미친다.

    자바가 스레드 스케줄링을 하는 방식은 우선순위(Priority)순환 할당(Round Robin)이 있다.

    우선순위

    • 예제 : PriorityExample.java - 우선순위에 의한 실행순서 테스트

      하드웨어의 차이인지.. 루프를 200억번은 해야 차이가 나타났다.

    • 우선순위가 높은 스레드가 더 많은 실행 상태를 가지도록 스케줄링한다.

    • 스레드 객체에 우선순위 번호를 부여할 수 있어서 개발자가 순서를 제어할 수 있다.

    • 우선순위는 1부터 10까지 있는데 숫자가 클수록 우선순위가 크다.

      • 우선순위는 상수로 등록되어 있다.

    순환 할당

    • 시간 할당량(Time Slice)를 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식으로 스케줄링한다.
    • JVM에 의해 정해져서 개발자가 코드로 제어할 수 없다.

     


     

    동기화 메소드와 동기화 블록

    공유 객체를 사용할 때의 주의할 점

    싱글 스레드일때는 한개의 스레드가 객체를 독차지할 수 있지만, 멀티 스레드 프로그램에서 객체를 공유하는 경우 조심해야할 것이 있다.
    A스레드는 공유객체의 값을 가져오려하고 B스레드는 공유객체의 값을 변경하려고 할 때, A스레드가 먼저 실행되는 경우 원하는 값을 얻을 수도 있고 B스레드가 먼저 값을 바꾸는 경우 A스레드는 예상과 다른 값을 얻어야 한다.

    (MainThreadExample.java) 실행 순서 설명

    // user1과 user2는 현재 같은 calculator를 참조하고 있다.
    user1.start(); 내부에서 Calculator의 setMemory(100)를 호출해 메모리 값을 100으로 바꾸고 user1 스레드가 2초간 멈춰있다. 
    user2.start(); user1 스레드가 멈춰있는 동안 마찬가지로 내부에서 setMemory(50)을 호출해 메모리 값을 50으로 바꾸고 user2 스레드가 2초간 멈춰있다.
    
    현재 user2 스레드에 의해 참조객체 calculator의 memory=50이 되었다. 
    2초뒤 user1 스레드와 user2 스레드가 차례대로 memory를 출력한다.
    user1 스레드는 100이 출력되길 기대했지만, 멈춰있는 동안 위에서 user2가 동일한 참조객체 calculator의 memory 값을 50으로 바꿔놓는 바람에 50이 출력되었다.

    이것이 멀티 스레드에서 공유객체를 사용할 때의 문제점이다.

    동기화 메소드 및 동기화 블록

    공유객체로 인한 문제를 발생시키지 않으려면, 사용중인 객체를 작업이 끝날 때까지 다른 스레드에서 접근하지 못하도록 잠금을 걸어야 한다.
    멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)라 하고 동기화(syncorinized)메소드와 동기화 블록을 사용한다.

    동기화 메소드는 메소드 전체에도 락을 걸 수 있고, 일부분에도 걸 수 있다.

    // 전체
    public synchronized void method() {
        //임계 영역
    }
    
    // 일부
    public void method() {
        // 여러 스레드 실행 가능 영역
        synchronized(공유객체) {
            //임계 영역
        }
        // 여러 스레드 실행 가능 영역
    }

    MainThreadExample.java 예제에서 Calculator.javasetMemory(int memory)synchronized만 붙여주면 문제를 해결할 수 있다. 물론 동기화 블록으로 만들어줘도 된다.

    댓글

Designed by Tistory.