ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 10주차 과제: 멀티쓰레드 프로그래밍
    Programming/Java live study 2021. 1. 21. 00:42
     

    10주차 과제: 멀티쓰레드 프로그래밍 · Issue #10 · whiteship/live-study

    목표 자바의 멀티쓰레드 프로그래밍에 대해 학습하세요. 학습할 것 (필수) Thread 클래스와 Runnable 인터페이스 쓰레드의 상태 쓰레드의 우선순위 Main 쓰레드 동기화 데드락 마감일시 2021년 1월 23일

    github.com

    목표

     자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.

    학습할 것 

     - Thread 클래스와 Runnable 인터페이스

     - 쓰레드의 상태

     - 쓰레드의 우선순위

     - Main 쓰레드

     - 동기화

     - 데드락


    0. 시작하기 전, 프로세스와 쓰레드

    0.1 프로세스 Process

     운영체제에서 실행중인 애플리케이션을 process라고 한다. 운영체제는 각각의 프로세스는 다른 프로세스에 접근이 불가능 하도록 설계되었다. 다른 프로세스의 메모리에 접근할 수 없고, 다른 프로세스가 소유한 자원에도 접근할 수 없다.

    0.2 쓰레드 Thread

     쓰레드는 실행 흐름이다. 한 프로세스는 적어도 하나의 실행 흐름을 소유하고 있다. 이것을 메인 쓰레드 (main thread) 라고 한다. 프로세스에 쓰레드가 두 개 이상인 것을 멀티 쓰레드 (multi thread) 라고 한다.

     

     실행 흐름은 객체도 아니고 메소드도 아니고, Java의 바이트코드를 순서대로 실행하는 것이 바로 실행 흐름이다. 

    0.3 Thread의 장단점

     쓰레드는 프로세스의 메모리와 자원을 다른 쓰레드와 공유한다. 쓰레드는 각각 Stack segment 영역을 각자 소유하고, 그 외의 메모리 영역이나 시스템 자원은 다른 스레드와 공유한다. 물론 다른 프로세스의 쓰레드와는 아무것도 공유하지 않는다.

     

     하지만 쓰레드들 사이에서 메모리와 자원을 공유하기 때문에 충돌이 발생할 수 있는 단점이 있다.


    1. Thread 클래스와 Runnable 인터페이스

     쓰레드를 생성하는 방법에는 클래스인 Thread 클래스를 상속하여 사용하는 방법과 인터페이스인 Runnable을 사용하는 방식이 있다.

    1.1 Thread 클래스

    Thread 클래스

    1.2 Runable 인터페이스

    Runnable 인터페이스

    1.3 Thread 클래스와 Runnable 인터페이스의 차이

     Therad 클래스도 결국 Runnable 인터페이스를 구현한다. 만들고자 하는 클래스를 Runnable 인터페이스로 구현할 경우, 추가적으로 클래스를 상속이 가능하다. Thread 클래스를 상속할 경우 클래스에서 다중상속을 지원하지 않기 때문에 추가적인 클래스 상속이 불가능할 것이다.

    1.4 사용해보자

    public class ThreadExample extends Thread {
    
        int number;
    
        public ThreadExample(int number) {
            this.number = number;
        }
    
        @Override
        public void run() {
            System.out.println(number);
        }
    }

     

     

     간단하게 number를 출력하는 Thread 클래스를 선언하였다.

     

    public class Main {
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                Thread thread;
                if (i % 2 == 0) {
                    thread = new ThreadExample(1);
                    thread.start();
                } else if (i % 2 != 0) {
                    thread = new ThreadExample(0);
                    thread.start();
                }
            }
        }
    }
    

     

     짝수일 때 0을 출력하고 홀수 일 때 1을 출력하도록 구현한 메인 메소드이다. 이때 0과 1이 번갈아 가면서 출력될 것을  예상하지만 결과를 보면 서로 뒤섞여서 출력된 것을 알 수 있다. 

     

    0
    1
    1
    1
    0
    1
    0
    1
    0
    ...

     

     다수의 쓰레드가 생성되면서 병렬로 동시에 실행된 것으로 추측된다.

     

    Thread thread = new ThreadExample();

     

     

     이것은 단순히 Thread 객체를 생성한 것이다. 이 Thread 객체는 Java 객체일 뿐, 실행 흐름이 아니다. 이 객체는 새로운 실행 흐름을 만들고 제어하기 위한 메소드가 제공되는 객체이다.

     

    thread.start();

     

     쓰레드를 실행하기 위해서는 start() 메소드를 사용해야 한다. start() 메소드를 호출한 시점에 새로운 쓰레드가 생성된다. 새 실행 흐름은 객체에 run 메소드를 실행하고 run 메소드가 리턴될 때 실행 흐름이 종료된다. 

     

     밑의 코드는 Thread를 Runnable로 구현한 예제이다.

     

    public class RunnableExample implements Runnable {
    
        int number;
    
        public RunnableExample(int number) {
            this.number = number;
        }
    
        @Override
        public void run() {
            System.out.println(number);
        }
    }

     

    public class Main {
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                Thread thread1 = new Thread(new RunnableExample(0));
                Thread thread2 = new Thread(new RunnableExample(1));
                if (i % 2 == 0) {
                    thread1.start();
                } else if (i % 2 != 0) {
                    thread2.start();
                }
            }
        }
    }
    

    1.5 Thread 클래스의 메소드

    Thread.yield()

     - static void 메소드이고, 현재 쓰레드를 정지하고 다른 쓰레드에게 실행을 양보한다.

     

    thread.join();

     - 특정 쓰레드가 끝날 때 까지 대기하고 그 쓰레드가 종료되면 현재 쓰레드를 실행한다.

     

    thread.sleep(long millis)

     - 매개 변수로 넘어온 시간 만큼 대기한다.


    2. 쓰레드의 상태

     Thread 클래스는 enum 타입으로 총 6가지의 상태를 가지고 있다.

     

    NEW 

     - 쓰레드가 생성된 경우 New 상태이고 아직 실행되지 않았다.

     - strat() 메소드를 호출하여 상태를 변경한다.

     

    RUNNABLE

     - 실행 가능한 쓰레드임을 나타낸다.

     - JVM 쓰레드의 스케줄러에서 관리된다.

     - 같은 운영 체제의 다른 리소스를 기다릴 수 있다.

     

    BLOCKED

     - 쓰레드가 blocked된 상태이다. 

     - 객체의 lock이 풀릴 때 까지 대기한다.

     

    WAITING

     - 다른 쓰레드가 통제할 때 까지 기다린다.

     

    TIMED_WATING

     - WAITING과 다른 점은 주어진 시간 동안 기다린다.

     

    TERMINATED 

     - 실행을 마친 상태이다.

     

    상태도


    3. 쓰레드의 우선순위

     둘 이상의 쓰레드를 사용하는 멀티스레드 환경에서는 우선순위를 부여하여 어떠한 순서로 실행할지 결정해야 한다. 둘 이상의 작업을 동시에 하는 것이 아니라 번갈아 가면서 작업이 전환된다.

     

    thread.setPriority(우선 순위);

     

     우선순위의 매개변수는 1 ~ 10으로 직접 지정하거나 Thread 클래스에 있는 상수를 사용한다.

     

    public static final int MIN_PRIORITY = 1;
    public static final int NORM_PRIORITY = 5;
    public static final int MAX_PRIORITY = 10;

    4. Main 쓰레드

     Java에서 JVM이 애플리케이션을 실행하면서 적어도 하나의 쓰레드를 생성하는데 그것이 바로 Main 쓰레드이다. Main 쓰레드는 main() 메소드의 코드를 순차적으로 실행하고 main 메소드가 끝나면 종료된다.

     

    public class Main {
        public static void main(String[] args) {
            // Main 쓰레드 시작
            ...
            // Main 쓰레드 종료
        }
    }
    

    5. 동기화

    5.1 synchronized

     동기화는 프로세스 혹은 쓰레드가 수행되는 시점에 서로 알고 있는 데이터나 정보가 일치해야 한다. 멀티 쓰레드 환경에서 여러 쓰레드가 공유 객체를 사용하게 되면 해당 데이터가 변질될 우려가 있다. 그렇기 때문에 하나의 쓰레드가 사용 중인 객체는 다른 쓰레드가 접근할 수 없도록 해야 한다.

     

     Java에서 이러한 동기화를 위한 블록과 메소드를 제공한다.

     

    synchronized(객체) {
        // 다른 실행 흐름의 접근을 막기 위해 겍체를 lock한다.
    }

     

     객체가 lock 되어 있는 동안에는 그 객체를 lock한 메소드만 호출이 가능하다. 다른 쓰레드도 호출은 가능하지만, unlock 상태가 될 때 까지 대기 상태에 있는다.

     

     메소드 선언 부분에도 synchronized 키워드를 붙일 수 있다. synchronized 키워드가 붙은 메소드를 어느 한 실행 흐름이 실행한다면 해당 객체를 lock된다.

    5.2 synchronized의 대안

     synchronized 블록과 메소드는 lock, unlock을 하며 thread safe를 유지한다. 하지만 해당 작업은 매무 무거운 작업이기 때문에 성능면에서 좋지 않다.

     

     만약 lock, unlock을 위한 대상이 기본 자료형이면 AtomicInteger나 AtomicDouble 클래스를 사용하면 thread safe하게 유지 가능하다.

     

    5.3 thread safe한 클래스

     또한 Java에서는 thread safe를 보장하는 표준 라이브러리와 immutable 클래스가 있다. 

     

     - Vector

     - Stack

     - HashTable

     - StringBuffer 

     

    등등 클래스 내부를 살펴보면 synchronized 키워드가 들어 있는 것을 알 수 있다.

    5.4 immutable 클래스

     immutable 클래스란, 객체를 생성한 후 내부의 값을 바꿀 수 없는 클래스이다. immutable한 클래스는 내부의 값이 바뀔 수 없기 때문에 lock, unlock 작업을 수행하지 않아도 thread safe하게 유지할 수 있다.


    6. 데드락

    출처 : tutorials.jenkov.com/java-concurrency/deadlock.html

     

      deadlock 교착상태. 둘 이상의 쓰레드가 lock을 획득하기 위해 기다린다. 이 lock을 잡고 있는 쓰레드도 똑같이 다른 lock을 기다리며 서로 block 상태에 놓이는 것을 말한다. deadlock은 여러 쓰레드가 동시에 동일한 lock을 획득하려 할 때 발생할 수 있다.

     

     예를 들면 실행 흐름 A가 객체 x를 lock했고, 객체 y를 lock하려다가 이미 다른 객체가 y 객체를 lock했기 때문에 block 상태가 된다. y 객체가 unlock 될 때 까지 실행 흐름 A는 block 상태에 빠진다.

     

     그런데 y 객체를 lock하던 실행 흐름 B는 x 객체를 lock하기 위해 시도한다. 하지만 x 객체는 이미 실행 흐름 A에 의하여 lock 되어 있기 때문에 실행 흐름 B도 block 상태에 빠지게 된다.

     

     실행 흐름 A와 실행 흐름 B는 강제 종료 될 때까지 영원히 대기 상태에 빠지게 된다.

     

     데드락 상황을 회피하기 위해선 최대한 lock과 unlock을 피해야 한다. synchronized 블록의 범위를 최소화 하거나 없어도 되는 코드들은 블록 밖으로 꺼낸다. 그럼에도 불구하고 여러 객체에서 lock해야 한다면 객체에 대한 순서를 정하여 조절해야 한다. 


     

     

    'Programming > Java live study' 카테고리의 다른 글

    12주차 과제: 애노테이션  (0) 2021.02.03
    11주차 과제: Enum  (0) 2021.01.27
    9주차 과제: 예외 처리  (0) 2021.01.13
    8주차 과제: 인터페이스  (0) 2021.01.05
    7주차 과제: 패키지  (0) 2020.12.30

    댓글

Designed by Tistory.