[10] Java - MultiThread Programing

harold·2021년 3월 6일
0

java

목록 보기
3/13

학습할 것 (필수)

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

Thread 클래스와 Runnable 인터페이스


스레드의 개념

프로세스(process)란?

프로세스(process)란 단순히 실행 중인 프로그램(program)이라고 할 수 있습니다.

즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말합니다.

이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성됩니다.


스레드(thread)란?

스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.

모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다.

또한, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 합니다.


스레드의 생성과 실행

자바에서 스레드를 생성하는 방법에는 다음과 같이 두 가지 방법이 있습니다.

  1. Runnable 인터페이스를 구현하는 방법

  2. Thread 클래스를 상속받는 방법

두 방법 모두 스레드를 통해 작업하고 싶은 내용을 run() 메소드에 작성하면 됩니다.

다음 예제는 위의 두 가지 방법을 사용하여 스레드를 생성하고 실행하는 예제입니다.


예제

class ThreadWithClass extends Thread {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(getName()); // 현재 실행 중인 스레드의 이름을 반환함.

            try {

                Thread.sleep(10);          // 0.01초간 스레드를 멈춤.

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

class ThreadWithRunnable implements Runnable {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName()); // 현재 실행 중인 스레드의 이름을 반환함.

            try {

                Thread.sleep(10);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

 

public class Thread01 {

    public static void main(String[] args){

        ThreadWithClass thread1 = new ThreadWithClass();       // Thread 클래스를 상속받는 방법

        Thread thread2 = new Thread(new ThreadWithRunnable()); // Runnable 인터페이스를 구현하는 방법

 

        thread1.start(); // 스레드의 실행

        thread2.start(); // 스레드의 실행

    }

}

실행결과

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

위 예제의 실행 결과를 살펴보면, 생성된 스레드가 서로 번갈아가며 실행되고 있는 것을 확인할 수 있습니다.

Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없으므로, 일반적으로 Runnable 인터페이스를 구현하는 방법으로 스레드를 생성합니다.

Runnable 인터페이스는 몸체가 없는 메소드인 run() 메소드 단 하나만을 가지는 간단한 인터페이스입니다.

쓰레드의 상태


스레드로 객체를 생성하면 우선 실행 대기 상태로 들어갑니다. 실행대기상태란 아직 스케쥴링 전 즉 코어에서 작업을 할당받지 못한 상태입니다. 실행 대기 상태에 있는 스레드 중 스레드 스케쥴링으로 선택된 스레드가 CPU를 점유하고 run()을 실행합니다. 이때 CPU를 점유하고 있는 상태를 실행상태입니다. 아직 run()메소드가 끝나지 않아도 스레드 스케쥴링에 의해 다시 실행 대기 상태로 돌아가며 번갈아 가면서 내용을 끝낼 때까지 지속합니다. 경우에 따라 run중인 쓰레드를 일시 정지 시킬 수도 있는데 예제에서 많이 보았던 sleep() 메서드와 같은 경우입니다. 일시 정지 후에는 다시 실행 대기 상태로 갑니다.

스레드 상태 제어Permalink
실행 중인 스레드 상태를 변경하는 것을 스레드 상태 제어라고 합니다. 멀티 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요합니다. 상태를 제어하는 메소드는 아래 그림과 같습니다. 취소선을 가지는 메소드들은 스레드의 안정성을 해칠 수 있다고 하여 더는 사용하지 않도록 권장되는 Deprecated 메소드들 이기 때문에 사용하지 않는 것이 좋습니다.

쓰레드의 우선순위


자바에서 각 스레드는 우선순위(priority)에 관한 자신만의 필드를 가지고 있습니다.

이러한 우선순위에 따라 특정 스레드가 더 많은 시간 동안 작업을 할 수 있도록 설정할 수 있습니다.

getPriority()와 setPriority() 메소드를 통해 스레드의 우선순위를 반환하거나 변경할 수 있습니다.

스레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이며, 숫자가 높을수록 우선순위 또한 높아집니다.

하지만 스레드의 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값일 뿐입니다.

우선순위가 10인 스레드가 우선순위가 1인 스레드보다 10배 더 빨리 수행되는 것이 아닙니다.

단지 우선순위가 10인 스레드는 우선순위가 1인 스레드보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받을 뿐입니다.

그리고 스레드의 우선순위는 해당 스레드를 생성한 스레드의 우선순위를 상속받게 됩니다.

예제

class ThreadWithRunnable implements Runnable {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName()); // 현재 실행 중인 스레드의 이름을 반환함.

            try {

                Thread.sleep(10);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

 

public class Thread02 {

    public static void main(String[] args){

        Thread thread1 = new Thread(new ThreadWithRunnable());

        Thread thread2 = new Thread(new ThreadWithRunnable());

 

①      thread2.setPriority(10); // Thread-1의 우선순위를 10으로 변경함.

 

②      thread1.start(); // Thread-0 실행

③      thread2.start(); // Thread-1 실행

 

        System.out.println(thread1.getPriority());

        System.out.println(thread2.getPriority());

    }

}

실행결과

5

10

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

main() 메소드를 실행하는 스레드의 우선순위는 언제나 5이므로, main() 메소드 내에서 생성된 스레드 Thread-0의 우선순위는 5로 설정되는 것을 확인할 수 있습니다.

위의 예제는 ②번 라인에서 Thread-0이 먼저 실행되고, ③번 라인에서 Thread-1이 나중에 실행됩니다.

따라서 만약 ①번 라인이 존재하지 않는다면, Thread-0이 먼저 실행되고, Thread-1이 나중에 실행될 것입니다.

하지만 ①번 라인에서 Thread-1의 우선순위를 10으로 변경했기 때문에, Thread-1이 나중에 실행됐더라도 우선순위가 Thread-0보다 높아 먼저 실행되는 것입니다.

Main 쓰레드


모든 자바 어플리케이션은 Main Thread가 main() 메소드를 실행하면서 시작됩니다. 예외는 없습니다. 이러한 Main Thread 흐름 안에서 싱글 스레드가 아닌 멀티 스레드 어플리케이션은 필요에 따라 작업 쓰레드를 만들어 병렬로 코드를 실행할 수 있습니다. 싱글 스레드 같은 경우 메인 스레드가 종료되면 프로세스도 종료되지만, 멀티 스레드는 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않습니다.

public class Task implements Runnable {

    @Override
    public void run() {
        int sum = 0;
        for (int index = 0; index < 10; index++) {
            sum += index;
            System.out.println(sum);
        }
        System.out.println( Thread.currentThread() + "최종 합 : " + sum);
    }

}
public static void main(String args[]){
    Runnable task = new Task();
    Thread subTread1 = new Thread(task);
    Thread subTread2 = new Thread(task);
    subTread1.start();
    subTread2.start();
}

실행결과

// 스레드가 끝나는 순서는 랜덥입니다.
0
1
3
6
10
15
21
28
36
45
0
1
3
Thread[Thread-0,5,main]최종 합 : 45
6
10
15
21
28
36
45
Thread[Thread-1,5,main]최종 합 : 45

동기화


멀티스레드 환경에서의 문제는 스레드들이 객체를 공유하며 작업하는 경우가 생기기 때문에 A스레드와 B스레드가 공유하는 객체가 서로의 작업에 영향을 미치면 안 되기 때문에 이를 방지하는 방법으로 동기화 메소드와 동기화 블록 방법이 있습니다. 아래 코드는 서로에게 영향을 미쳐 원하는 결과를 가지고 오지 못하는 경우입니다. thred1 에서 예상하는 값은 100을 가지고 오는 것입니다.

// 공유 객체
public class ShareThread {
    private int value = 0;
    
    public void setValue( int value ) {
        this.value = value;
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(Thread.currentThread().getName() + "의 Value 값은 " + this.value +"입니다.");
        
    }
    
    public int getValue() {
        return value;
    }
}
public static void main(String args[]){
    ShareThread shareTread = new ShareThread();
    Thread thred1 = new Thread(()->{
        shareTread.setValue(100);
        
    });
    
    Thread thred2 = new Thread(()->{
        shareTread.setValue(10);
    });
    thred1.setName("스레드 1");
    thred2.setName("스레드 2");
    thred1.start();
    thred2.start();
}

실행결과

// 결과 
스레드 2Value 값은 10입니다.
스레드 1Value 값은 10입니다.

동기화 메소드 또는 동기화 블록 코드

스레드가 사용 중인 객체를 Lock을 걸어 해당 작업을 진행하는 Thread가 작업을 마칠 때까지 다른 쓰레드가 작업을 하지 못하게 하는 방법입니다. 메소드 선언에 synchronized를 붙이던가 ( 동기화 메소드 ) synchronized( 공유객체 ) { 작업 } 인 동기화 블록을 사용하면 됩니다.

// 동기화 메소드 
public synchronized void setValue( int value ) {
    this.value = value;
    try {
        Thread.sleep(2000);
    } catch (Exception e) {
    }
    System.out.println(Thread.currentThread().getName() + "의 Value 값은 " + this.value +"입니다.");  
}

// 동기화 블록 
public  void setValue( int value ) {
    synchronized ( this ) {
        this.value = value;
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(Thread.currentThread().getName() + "의 Value 값은 " + this.value +"입니다.");
    }
}

데드락


교착상태(Deadlock)

deadlock... 이름도 뭣같이 무섭네요. 교착상태라고 하는 것은 다중 프로그래밍 시스템에서 프로세스 혹은 스레드가 결코 일어나지 않을 일을 무한정으로 기다리는 상태를 말합니다. 시스템에서 교착상태는 프로세스가 요구하는 자원이 엉켜서 더 이상 작업을 더 실행할 수 없는 상태를 의미합니다.

시스템이라고 생각하면 조금 어려울 수 있는데 인간 세계에서도 똑같아요. 예를 들어볼까요?

평화로운 중고 사이트에서 A가 사고 싶은 노트북이 생겨서 B에게 연락을 합니다. 가뜩이나 요즘 중고사기가 판을 치는 시기에 B에게 먼저 노트북을 택배로 보내면 150만원을 입금해주겠다고 합니다. B 역시 A를 의심하여 먼저 입금하면 노트북을 보내겠다고 합니다.
A는 B의 노트북을, B는 A의 돈을 요구하는 상황이고 서로 주지않습니다. 이렇게 서로 자원(돈, 노트북)을 점유한 상태에서 막혀 버려 서로 해야할 일(거래)을 하지 못하는 것입니다.
아무 것도 아닌 일 때문에 해야할 일을 못하는 것이지요.

교착상태의 발생 조건

아래 설명에서는 프로세스만을 이야기하는데, 스레드도 같습니다. 편의상 프로세스만 예를 들어 이야기하도록 하겠습니다.

  1. 상호배제 (Mutual Exclusion)

한 번에 한 프로세스만 자원을 사용할 수 있어야합니다. 사용중인 자원을 다른 프로세스가 사용하려면 요청한 자원이 다 사용되고 난 후 해제될때까지 기다려야합니다.

  1. 점유와 대기 (Hold And Wait)

프로세스는 자원을 점유한 동시에 대기해야합니다. 위의 예에서 볼 수 있듯이 A는 돈을 소유하고 있고 B가 노트북을 주기까지 대기하고 있습니다.

  1. 비선점 (Non Preemptive)

자원을 선점할 수 없는 조건으로 누군가가 자원을 가지고 있을때 뺏을 수 없습니다. 위의 예에서도 지극히 정상적인 상식에서 A와 B는 물건을 불법적으로 훔칠 수 없죠.

  1. 순환(환형) 대기 (Circle wait)

프로세스가 여러개 있을때 자원의 점유와 요청의 관계가 순환되는 조건을 말합니다. 프로세스 3개를 각각 p1, p2, p3라고 할때 p1이 p2의 자원을 요청하고, p2가 p3의 자원을 요청하고, p3가 p1의 자원을 요청하고 그 자원은 요청한 자원을 얻을 수 있을때까지 반환할 수 없습니다. 이 조건은 위의 세 조건이 만족이 되면 자연스레 만족됩니다.

아래의 그림은 순환 대기를 나타낸 것인데 R1, R2, R3는 각 P1, P2, P3가 점유한 자원을 의미하고 각 자원은 소유된 프로세스쪽으로 화살표가 나가고 있습니다. 프로세스들의 화살표는 요구하는 자원쪽으로 향하고 있지요. 여기서 이 화살표들의 방향대로 나가면 사이클을 그리며 무한정 순환하게 됩니다

교착상태 발생 코드

아래의 코드는 교착상태를 스레드를 이용해서 발생한 예제입니다. 여기서는 간단히 메인 스레드와 메인 스레드에서 생성된 스레드를 고려합니다.

여기서 자원은 뮤텍스인 lock1과 lock2를 말하지요.

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>


pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void *function(void* arg){

        sleep(1);       //main thread가 먼저 실행할 여지를 줌
        printf("\t thread start\n");

        pthread_mutex_lock(&lock2);
        pthread_mutex_lock(&lock1);

        printf("\t critical section \n");

        pthread_mutex_unlock(&lock1);
        pthread_mutex_unlock(&lock2);

        printf("\t thread end\n");

}

int main(){
        pthread_t tid;

        printf("main thread start and create thread\n");
        pthread_create(&tid,NULL,function,NULL);

        pthread_mutex_lock(&lock1);
        sleep(5);               //thread가 lock2를 먼저 실행할 여유를 줌
        pthread_mutex_lock(&lock2);

        printf("critical section \n");

        pthread_mutex_unlock(&lock2);
        pthread_mutex_unlock(&lock1);

        pthread_join(tid,NULL);
        printf("main thread end\n");

}

(정확히 100% 아래처럼 동작한다고 말할 수는 없지만 위 코드는 대부분 제가 의도한 대로 동작합니다. 왜냐면 스레드 실행 순서의 흐름은 예측할 수 없으니까요.)

코드를 보면 각각 critical section을 pthread_mutex_lock을 통해 보호하고 있지요. 이 소스 코드에서 눈 여겨 봐야할 점은 메인 스레드가 우선 lock1을 점유하고, 이후 생성된 스레드는 lock2를 점유하고 있습니다.

바로 메인 스레드는 lock2를 점유하여 임계 영역에 진입하여야하는데 이미 생성된 스레드가 가지고 있습니다. 그렇기 때문에 lock2가 해제될때까지 기다립니다.

한편 생성된 스레드는 lock2는 점유했고 lock1을 얻은 후에 임계 영역을 진입하려합니다. 하지만 lock1은 이미 메인 스레드에 의해 점유되었습니다. 따라서 lock1이 해제될때까지 기다려야합니다.

여기서 위의 네가지 조건이 보이시나요?

첫번째 조건으로 critical section으로 특정 한 스레드만 임계 영역에 진입할 수 있습니다. 여기서 임계 영역이 중첩되어 있네요. lock1을 이용해 잠근 임계영역과 lock2를 통한 임계영역이지요.

두번째 조건으로 점유와 대기를 이루고 있습니다. 메인 스레드는 lock1을 점유하고 lock2가 해제되기를 기다리고 있고 생성된 스레드는 lock2를 점유하고 메인 스레드에 의해 lock1이 해제될때까지 기다리고 있습니다.

세번째 조건은 비선점 조건인데 서로 강제로 lock1, lock2를 가져올 수 없습니다.

네번째 조건은 위의 그림처럼 그려보면 사이클을 이루게 되죠.

교착상태 문제 해결

교착상태가 발생하면 어떻게 해결할 수 있을까요? 크게 세가지 방법이 있는데 간략히 알아보도록 합시다.

1) 교착 상태 예방

교착 상태가 발생하기 전에 조치를 취하는 방식으로 위의 4가지 조건 중 하나라도 발생시키지 않으면 됩니다.

  • 자원의 상호배제 조건 방지

  • 점유와 대기 조건 방지

  • 비선점 조건 방지

  • 순환 대기 조건 방지

앞서 얘기했듯이 처음 세조건이 만족되면 순환 대기가 발생하므로 결국에는 순환대기를 발생시키지 않으면 됩니다.

교착 상태 예방은 시스템의 처리량을 떨어뜨립니다.

2) 교착 상태 회피

예방처럼 교착 상태의 발생 가능성을 미리 제거하지 않고 교착상태가 일어난다고 보고 이것을 회피하는 것입니다. 예를 들어 몇 초동안 프로세스나 스레드가 임계 구역 전에 대기 중이라면 이 시간이 지난 후 다음 작업을 수행합니다. 위의 예제코드를 아래와 같이 수정할 수 있습니다.

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>


pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void *function(void* arg){
        struct timespec tout;
        struct tm* t;
        int locked;

        sleep(1);       //main thread가 먼저 실행할 여지를 줌
        printf("\t thread start\n");


        clock_gettime(CLOCK_REALTIME,&tout);
        tout.tv_sec += 5;

        pthread_mutex_lock(&lock2);
        locked = pthread_mutex_timedlock(&lock1,&tout);  //5초간만 lock이 걸림

        printf("\t critical section \n");

        if(locked == 0){
                pthread_mutex_unlock(&lock1);
        }
        pthread_mutex_unlock(&lock2);

        printf("\t thread end\n");

}

int main(){
        pthread_t tid;

        printf("main thread start and create thread\n");
        pthread_create(&tid,NULL,function,NULL);

        pthread_mutex_lock(&lock1);
        sleep(5);               //thread가 lock2를 먼저 실행할 여유를 줌
        pthread_mutex_lock(&lock2);

        printf("critical section \n");

        pthread_mutex_unlock(&lock2);
        pthread_mutex_unlock(&lock1);

        pthread_join(tid,NULL);
        printf("main thread end\n");

}

pthread_mutex_timed_lock 함수는 특정 시간동안 lock을 얻을 수 없다면 뮤텍스를 잠그지 않고 ETIMEDOUT이라는 오류 부호를 돌려줍니다. 따라서 스레드가 무한정 차단되지 않게 만듭니다. 이 코드를 실행시켜보면 아래와 같이 끝나기는 합니다.


# ./a.out
main thread start and create thread
         thread start
         critical section
critical section
         thread end
main thread end

교착 상태 회복

교착 상태를 시스템에서 탐지하여 회복시키는 알고리즘으로 교착상태를 회복합니다.

< 출처 >

TCPSchool.com
https://honbabzone.com/java/java-thread/
https://reakwon.tistory.com/120


0개의 댓글