슬기로운 개발생활

온라인 자바 스터디 #10 - 멀티쓰레드 프로그래밍

by coco3o
반응형

목표

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

학습할 것 (필수)

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

시작하기에 앞서, 프로세스와 쓰레드에 관해 가볍게 알고 넘어가자.

 

프로세스와 쓰레드

프로세스(process)란 간단히 말해 '실행 중인 프로그램'이다.

프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.

프로그램    ㅡ실행ㅡ>   프로세스

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

모든 프로세스에는 최소 하나 이상의 쓰레드가 존재하며,

 

둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라고 한다.

멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이 가능하다.

실제로 한 개의 CPU가 한 번에 단 한가지 작업만 수행할 수 있기 때문에 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 동시에 여러 작업이 수행되는 것처럼 보이게 하는 것이다. 그래서 프로세스의 성능이 쓰레드의 개수에 비례하지 않으며,

하나의 쓰레드를 가진 프로세스보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수도 있다.

 

멀티쓰레딩의 장점

  • CPU의 사용률 향상
  • 자원을 보다 효율적으로 사용
  • 사용자에 대한 응답성 향상
  • 작업이 분리되어 코드가 간결해짐

Thread 클래스와 Runnable 인터페이스

쓰레드를 구현하는 방법은 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법,  2가지가 있다.

둘 중 어느 쪽을 사용해도 별 차이는 없지만 Java에서는 다중 상속을 허용하지 않기 때문에,

Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없어서 Runnable 인터페이스를 구현하는 방법이 일반적이다.

 

Runnable  인터페이스를  구현하는 방법은  재사용성이 높고  코드의  일관성을  유지할  수  있다는  장점이  있기  때문에 

보다  객체지향적인 방법이라 할 수 있다.

Runnable인터페이스는 run()메소드만 정의되어 있는 간단한 인터페이스이다.

1. Thread클래스를 상속
public class MyThread extends Thread {
           public void run() { /* 작업내용 */}          // Thread 클래스의 run()을 오버라이딩
}


2. Runnable인터페이스를 구현
public class MyThread implements Runnable {
@Override
public void run() { /* 작업내용 */}          //Runnable인터페이스의 추상메소드 run()을 구현
}


Thread클래스와 Runnable인터페이스 구현하기

Thread 클래스 확장하기

첫번째 방법으로 java.lang.Thread 클래스를 확장할 수 있다. Thread 클래스에는 상당히 많은 메소드가 있는데,

그 중에서 run() 이라는 메소드만 오버라이드 해주면 된다.

import java.util.Random;

public class MyThread extends Thread {

    private static final Random random = new Random();

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("- " + threadName + " has been started");
        int delay = 1000 + random.nextInt(4000);

        try {
            Thread.sleep(delay);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
    }
}

쓰레드마다 수행 시간을 다르게 해보고싶어서 Thread.sleep( )메소드를 이용하여 1초 이상 6초 미만의 랜덤 딜레이를 주었다.

그리고 각 쓰레드의 시작과 종료 시점에 Thread.currentThread( ).getName( )메소드를 통해 쓰레드 이름이 출력되도록 하였다.

이 쓰레드 이름은 String을 인자로 받는 생성자를 통해 객체 생성 시점에 셋팅될 것이다.

 

Runnable 인터페이스 구현하기

Thread 확장 예제와 동일한 기능을 Runnable 인터페이스를 구현하여 작성한다.

클래스 뒤에 Extends Thread에서 implements Runnable 로 바뀐 것 빼고는 동일한 코드이다.

import java.util.Random;

public class MyRunnable implements Runnable {

    private static final Random random = new Random();
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("- " + threadName + " has been started");
        int delay = 1000 + random.nextInt(4000);

        try {
            Thread.sleep(delay);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
    }
}

 

실행해보기

위의 작성한 클래스의 쓰레드 실행 방법은 약간 다르다.

두 가지 클래스 모두 Thread 클래스의 start( )메소드를 통해 실행시킬 수 있는데, 

Thread 를 확장한 MyThread 클래스의 경우, 해당 객체에 start( ) 메소드를 직접 호출할 수 있다.

반면에 Runnable 을 구현한 MyRunnable 클래스의 경우, Runnable 형 인자를 받는 생성자를 통해 별도의 Thread 객체를 생성 후 start( ) 메소드를 호출해야 한다.

public class ThreadRunner {
    public static void main(String[] args) {
        // create thread objects
        Thread thread1 = new MyThread();
        thread1.setName("Thread #1");
        Thread thread2 = new MyThread();
        thread2.setName("Thread #2");

        // create runnable objects
        Runnable runnable1 = new MyRunnable();
        Runnable runnable2 = new MyRunnable();

        Thread thread3 = new Thread(runnable1);
        thread3.setName("Thread #3");
        Thread thread4 = new Thread(runnable2);
        thread4.setName("Thread #4");

        // start all threads
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

아래 실행 결과를 보면 4개의 쓰레드가 순차적으로 실행되지 않고, 랜덤 딜레이 때문에 끝나는 시간도 재각기인 것을 알 수 있다.

그리고 매번 실행할 때마다 딜레이가 달라지기 떄문에, 실행 결과가 항상 동일하지 않을 것이다.

- Thread #4 has been started
- Thread #3 has been started
- Thread #1 has been started
- Thread #2 has been started
- Thread #1 has been ended (1027ms)
- Thread #4 has been ended (1384ms)
- Thread #3 has been ended (2127ms)
- Thread #2 has been ended (2986ms)

Process finished with exit code 0

 

Thread vs. Runnable

위의 코드를 보면 Thread 클래스를 확장하는 것이 실행 방법이 조금 더 간단하다는 것을 볼 수 있다.

하지만 Thread 클래스를 확장하는 클래스는 다른 클래스를 상속받을 수 없기 때문에,

Runnable 인터페이스를 구현하는 것이 더 바람직할 것이다.

실제로 많은 개발자들이 대부분 상황에서 Thread 클래스를 확장하기보다는 Runnable 인터페이스를 구현하는 것을 선호한다고 한다. 

 

Lambda를 이용하여 Thread 실행

Runnable 인터페이스를 구현해서 해당 클래스의 객체 생성하는 방법보다 좀 더 간단하게 Runnable 인터페이스를 사용할 수 있다.

바로 Java 8의 람다 구문을 이용하는 것이다. 다음과 같이 Thread의 생성자의 인자로 람다를 넘기면 된다.

public class RunnableLambdaExample {

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

}

Thread의 상태

쓰레드 프로그래밍을 하다보면 동기화/비동기화 뿐만 아니라 쓰레드의 행동을 직접 제어해야 할 경우가 생긴다.

예를 들어, 1000장의 문서를 복사해야한다고 가정해보자. 

공유된 프린터는 1개 밖에 없는데, 이 때 1000장을 전부 복사하는 동안 혼자서 쓸 수 없을 것이다.

다른 사람들도 프린터를 사용해야 하는 경우가 있기 때문에 그럴 때, 100장씩 복사를 하도록 하고,

그 때마다 다른 사람들이 사용하도록 통제할 수도 있을 것이다.

 

 

쓰레드 상태
  • 쓰레드는 start( ) 하게 되면 일반적으로 다음과 같은 상태로 진행 된다.

출처 : https://widevery.tistory.com/27

  • 경우에  따라  쓰레드는  실행  상태에서  실행  대기  상태로  가지  않을 수도  있다.
  • 실행  상태에서  일시정지  상태로  가기도  하는데,  일시정지  상태는  쓰레드가  실행될  수  없는  상태이다.
  • 쓰레드가  다시  실행  상태로  가기  위해선  일시정지  상태에서  실행  대기  상태로  가야한다.

출처 : https://widevery.tistory.com/27

이처럼 쓰레드도 상태를 제어해야 할 필요성이 있는데, 제어를 하기 전에 해당 쓰레드의 상태를 알아야 한다.

그러기 위해서는 getState( ) 메소드를 사용하여 상태를 확인한다. 

 

쓰레드의 상태는 크게 6가지가 있다.

  •  NEW   :  쓰레드가  생성된  상태,  아직  start( ) 되지  않음
  •  RUNNABLE   :  start( )  가  호출되어  실행  대기, run()  하면  RUNNING(CPU  점유)이  된다. Runnable pool에  모여있다.
  •  WAITING   :  일시  정지,  주어진  시간동안  기다림
  •  BLOCK   :  일시  정지,  사용하려는  객체의  Lock이  풀릴때  까지  대기 
  •  TERMINATED   :  실행마치고  종료, run()이  끝나면  TERMINATED  되면서  소멸

아래는 쓰레드의 상태를 출력하는 StatePrintThread 클래스이다.

public class StatePrintThread extends Thread {

    private Thread targetThread;

    public StatePrintThread(Thread targetThread) {
        this.targetThread = targetThread;
    }

    @Override
    public void run() {
        while(true) {
            // Thread State get
            Thread.State state = targetThread.getState();
            System.out.println("Thread State : " + state);

            // NEW State? => RUNNABLE
            if(state == State.NEW) {
                targetThread.start();
            }

            // TERMINATED? => end while
            if(state == State.TERMINATED) break;

            try {
                Thread.sleep(500);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

다음은 TargetThread 클래스이다.

10억 번 for문을 돌아 RUNNABLE 상태를 유지하고, Thread.sleep( )메소드를 호출해서

1.5초간 TIMED_WAITING 상태를 유지하고, 다시 10억 번 for문을 돌아 RUNNABLE 상태를 유지한다.

public class TargetThread extends Thread {

    @Override
    public void run() {
        for(long i = 0; i < 1000000000; i++) {}

        try {
            Thread.sleep(1500);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

TargetThread가 객체로 생성되면 NEW 상태를 가지고, run( )메소드가 종료되면 TERMINATED 상태가 되므로

다음과 같은 순서로 변한다.

NEW ㅡ> RUNNABLE ㅡ> TIMED_WAITING ㅡ> RUNNABLE ㅡ> TERMINATED

ThreadStateExample.java ㅡ 실행 클래스

public class ThreadStateExample {
    public static void main(String[] args) {
        StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());

        statePrintThread.start();
    }
}
Thread State : NEW
Thread State : RUNNABLE
Thread State : TIMED_WAITING
Thread State : TIMED_WAITING
Thread State : TIMED_WAITING
Thread State : RUNNABLE
Thread State : TERMINATED

Process finished with exit code 0

Thread의 우선순위

쓰레드는 우선순위라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.

쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.

 

Thread의 우선순위와 관련된 메소드와 필드
void setPriority(int newPriority)  :  쓰레드의  우선순위를  지정한  값으로  변경한다.
int getPriority()  :  쓰레드의  우선순위를  반환한다.

public static final int MAX_PRIORITY = 10 //최대  우선  순위
public static final int MIN_PRIORITY = 1  //최소  우선  순위
public static final int  NORM_PRIORITY = 5  //보통  우선  순위

쓰레드가  가질  수  있는  우선순위의  범위는  1~10이며  숫자가  높을  수록  우선순위가  높다.

그러나  우선순위의  높고  낮음은  절대적인  것이  아니라  상대적인  것임에  주의하자.

쓰레드의  우선순위는  쓰레드를  생성한  쓰레드로부터  상속  받는다는  것이다.

main메소드를  수행하는  쓰레드는  우선순위가  5이므로  main메소드  내에서  생성하는  쓰레드의  우선순위는  자동적으로  5이다.

 

Priority의 활용 범위
Priority의 범위 사용 유형
10 위기관리
7 ~ 9 상호작용, 이벤트 관리
4 ~ 6 IO관련 작업
2 ~ 3 백그라운드 작업
1 기타 작업

 

아래의 코드는 두 개의 쓰레드의 우선순위를 다르게 설정하는 예제이다.

public class ThreadPriority {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        // Default Priority 5
        thread1.setPriority(2);
        thread2.setPriority(7);

        System.out.println("Priority of thread1 (-) : " + thread1.getPriority());
        System.out.println("Priority of thread2 (|) : " + thread2.getPriority());

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

class Thread1 extends Thread {
    public void run() {
        for(int i = 0; i < 20; i++) {
            System.out.print("-");
            for(int x = 0; x < 1000000000; x++);
        }
    }
}

class Thread2 extends Thread {
    public void run() {
        for(int i = 0; i < 20; i++) {
            System.out.print("|");
            for(int x = 0; x < 20; x++);
        }
    }
}
Priority of thread1 (-) : 2
Priority of thread2 (|) : 7
-||||||||||||||||||||-------------------
Process finished with exit code 0

thread1과 thread2 모두 main 메소드에서 생성하였기 때문에 main 메소드를 실행하는 쓰레드의 우선순위인 5를 상속받았다.

그 다음에 thread2.setPriority(7)로 thread2의 우선순위를 7로 변경한 다음 start( )를 호출해서 쓰레드를 실행시켰다.

setPriority( ) 메소드는 쓰레드를 시작하기 전에만 우선순위를 변경할 수 있다.


Main Thread

Java에서 main( )메소드는 프로그램의 시작점이다.

그리고 main( )메소드 또한 쓰레드에 의해 실행되는데 이 쓰레드를 메인쓰레드라 한다.

메인 쓰레드는 main( )메소드를 실행하며 시작된다. 즉, 메인 쓰레드는 main메소드의 코드 흐름이다.

public static void main(String[] args) { //Main Thread Start
		...
        ...
        ...
}  //Main Thread End        

싱글  쓰레드  같은  경우  main  Thread가  종료되면  프로세스도  종료되지만,

멀티  쓰레드는  main  Thread가  종료되더라도  실행중인  Thread가  하나라도  있다면  프로세스는  종료되지  않는다.

 

 

데몬 쓰레드

데몬 쓰레드는 메인 쓰레드와 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 쓰레드는 메인 쓰레드와 

일반 쓰레드의 보조 역할을 수행하므로 일반 쓰레드가 모두 종료되면 데몬 쓰레드의 존재의 의미가 없기 때문이다.

 

데몬 쓰레드의 예로는 가비지 컬렉션, 워드 프로세서의 자동저장, 화면 자동 갱신 등이 있다.

데몬 쓰레드는 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면

작업을 수행하고 다시 대기하도록 작성한다.

데몬 쓰레드는 일반 쓰레드의 작성 방법과 실행 방법이 같으며, 다만 쓰레드를 생성한 다음

실행하기 전에 setDaemon(true)를 호출하기만 하면 된다.쓰레드 실행 전에 반드시 데몬 쓰레드로 설정해야 한다.

boolean isDaemon()   //쓰레드가  데몬  쓰레드인지  확인한다.  데몬 쓰레드이면  true  반환
void setDaemon(boolean on)   //쓰레드를  데몬  쓰레드  또는  사용자  쓰레드로  변경한다.(매개변수 on의 값을 true로 지정하면  데몬  쓰레드가  된다.)
public class DaemonExample implements Runnable {
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonExample());
        thread.setDaemon(true); //이 부분이 없으면 종료되지 않음
        thread.start();

        for(int i = 1; i <=20; i++) {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
            if(i == 5) autoSave = true;
        }
        System.out.println("TERMINATED");
    }
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(3 * 1000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(autoSave) autoSave();
        }
    }

    private void autoSave() {
        System.out.println("작업 파일이 자동 저장 되었습니다.");
    }
}
1
2
3
4
5
작업 파일이 자동 저장 되었습니다.
6
7
8
작업 파일이 자동 저장 되었습니다.
9
10
11
작업 파일이 자동 저장 되었습니다.
12
13
14
작업 파일이 자동 저장 되었습니다.
15
16
17
작업 파일이 자동 저장 되었습니다.
18
19
20
TERMINATED

Process finished with exit code 0

3초마다 변수 autoSave의 값을 확인해서 그 값이 true면 autoSave()를 호출하는 일을 무한히 반복하도록 쓰레드를 작성하였다.

만일 이 쓰레드를 데몬 쓰레드로 설정하지 않았다면, 이 프로그램은 강제종료하지 않는 한 영원히 종료되지 않을 것이다.

setDaemon 메소드는 반드시 start()를 호출하기 전에 실행되어야 한다.


동기화

싱글쓰레드 프로세스의 경우 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데 별 문제가 없지만,

멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업을 하기 때문에 서로 작업에 영향을 주게 된다.

만일 쓰레드 A가 작업하던 도중 다른 쓰레드B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경하였다면, 다시 쓰레드 A가 제어권을 받아 나머지 작업을 마쳤을 때 의도했던 것과는 다른 결과를 얻을 수도 있다.

 

synchronized를 이용한 동기화

자바에서는 키워드 synchronized를 통해 해당 작업과 관련된 공유 데이터에 Lock을 걸어서 먼저 작업 중이던 쓰레드가 작업을 완전히 마칠 때까지는 다른 쓰레드에게 제어권이 넘어가더라도 데이터가 변경되지 않도록 보호함으로써 쓰레드의 동기화를 가능하게 한다.

 

  • 특정 객체에 lock을 걸고자 할 때
synchronized(객체의 참조변수) {
          //.....
}

synchronized블록의 경우 지정된 객체는 synchronized블록의 시작부터 lock이 걸렸다가 블록이 끝나면 lock이 풀린다.

이 블록을 수행하는 동안은 지정된 객체에 lock이 걸려서 다른 쓰레드가 이 객체에 접근할 수 없게된다.

 

  • 메소드에 lock을 걸고자 할 때
public synchronized void Example() {
           //.....
}

synchronized 메소드의 경우에도 한 쓰레드가 synchronized 메소드를 호출해서 수행하고 있으면,

이 메소드가 종료될 때 까지 다른 쓰레드가 이 메소드를 호출하여 수행할 수 없게된다.

 

아래의 예제를 보자.

public class ThreadEx {
    public static void main(String[] args) {
        Runnable r = new A();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);

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

class Account {
    int balance = 1000;

    public void withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000);} catch (Exception e) { }
            balance -= money;
        }
    }
}

class A implements Runnable {
    Account acc = new Account();
    public void run() {
        while(acc.balance > 0) {
            //100, 200, 300중의 한 값을 임의로 선택하여 출금(withdraw)
            int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("balance : " + acc.balance);
        }
    }
}
balance : 800
balance : 800
balance : 600
balance : 500
balance : 300
balance : 100
balance : 0
balance : -100

Process finished with exit code 0

실행결과를 보면 잔고(balance)가 음수인 것을 알 수 있다. 그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에

다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다. 다시 이전의 쓰레드로 제어권이 넘어오면 if문 다음부터 수행하게 되므로 

확인하는 if문과 출금하는 문장은 하나로 동기화블록으로 묶어져야 한다.

예제에서는 상황을 보여주기 위해 일부러 Thread.sleep(1000)을 사용해서 if문을 통과하자마자 다른 쓰레드에게 제어권을

넘기도록 하였지만, 이렇게 하지 않더라도 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.


class Account {
    int balance = 1000;
        //synchronized 키워드를 붙이기만 하면 간단히 동기화가 된다.
    public synchronized void withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000);} catch (Exception e) { }
            balance -= money;
        }
    }
}
balance : 700
balance : 400
balance : 300
balance : 200
balance : 100
balance : 0
balance : 0

Process finished with exit code 0

한 쓰레드에 의해서 먼저 withdraw()가 호출되면, 종료될 때 까지 다른 쓰레드가 withdraw()를 호출하더라도

대기상태에 머물게 된다. 즉, withdraw()는 한 순간에 단 하나의 쓰레드만 사용할 수 있다는 것이다.

 

만일 withdraw()가 수행되는 동안 객체에 lock을 걸고자 한다면 다음과 같이 할 수도 있다.

class Account {
    int balance = 1000;
    public void withdraw(int money) {
        synchronized (this) {
            if (balance >= money) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                }
                balance -= money;
            }
        }
    } //withdraw
}

데드락

출처 : https://includestdio.tistory.com/12

데드락이란, 교착상태를 말하고, 두 개의 쓰레드가 하나씩 자원을 소지하고 있으면서 상대방이 가진 자원을

서로 원하고 있는 상태를 말한다. 어떤 작업도 실행되지 못하고 계속 서로 상대방의 작업이 끝나기만을 바라는 무한정 대기상태이다.

 

데드락의 발생조건

교착상태는 한 시스템 내에서 다음의 네 가지 조건이 동시에 성립 할 때 발생한다.

따라서, 아래의 네가지 조건 중 하나라도 성립하지 않도록 만든다면 교착상태를 해결할 수 있다.

 

  • 상호 배제 (Mutual exclusion) - 자원은 한 번에 한 프로세스만이 사용할 수 있어야 한다.
  • 점유대기 (Hold and wait) - 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야 한다.
  • 비선점 (No preemption) - 다른 프로세스에 할당된 자원은 사용이 끝날 때 까지 강제로 빼앗을 수 없어야한다.
  • 순환 대기 (Circular wait) - 프로세스의 집합 {P0, P1, ,…Pn}에서 P0는 P1이 점유한 자원을 대기하고 P1은 P2가 점유한 자원을 대기하고 P2…Pn-1은 Pn이 점유한 자원을 대기하며 Pn은 P0가 점유한 자원을 요구해야 한다.

데드락 처리
  • 교착 상태 예방 및 회피 - 교착 상태가 되지 않도록 보장하기 위하여 교착 상태를 예방하거나 회피하는 프로토콜을 이용하는 방법
  • 교착 상태 탐지 및 회복 - 교착 상태가 되도록 허용한 다음에 회복시키는 방법
  • 교착 상태 무시 - 대부분의 시스템은 교착 상태가 잘 발생하지 않으며, 교착 상태 예방, 회피, 탐지, 복구하는 것은 비용이 많이 듦.

교착 상태 예방 (Prevention)

- 교착 상태 발생 조건 중 하나를 제거함으로써 해결하는 방법

- 자원의 낭비가 심하다.

 

  • 상호 배제 (Mutual exclusion) 부정 - 여러 개의 프로세스가 공유 자원을 사용할 수 있도록 한다.
  • 점유 대기 (Hold and wait) 부정 - 프로세스가 실행되기 전 필요한 모든 자원을 할당한다.
  • 비선점 (No Preemption) 부정 - 자원을 점유하고 있는 프로세스가 다른 자원을 요구할 때 점유하고 있는 자원을 반납하고, 요구한 자원을 사용하기 위해 기다리게 한다.
  • 순환 대기 (Circular wait) 부정 - 자원에 고유한 번호를 할당하고, 번호 순서대로 자원을 요구하도록 한다.

 

교착 상태 회피 (Avoidance)

- 교착 상태가 발생하면 피해나가는 방법

- 은행원 알고리즘 (Banker's Algorithm)

 

  • 프로세스가 자원을 요구할 때 시스템은 자원을 할당한 후에도 안정 상태로 남아있게 되는지를 사전에 검사하여 교착 상태를 회피하는 기법
  • 안정 상태에 있으면 자원을 할당하고, 그렇지 않으면 다른 프로세스들이 자원을 해지할 때까지 대기함

 

교착 상태 회복 (Recovery)

-교착 상태를 일으킨 프로세스를 종료하거나, 할당된 자원을 해제함으로써 회복하는 것을 의미

 

  • 프로세스를 종료하는 방법
    • 교착 상태의 프로세스를 모두 중지
    • 교착 상태가 제거될 때까지 한 프로세스씩 중지
  • 자원을 선점하는 방법
    • 교착 상태의 프로세스가 점유하고 있는 자원을 선점하여 다른 프로세스에게 할당하며, 해당 프로세스를 일시 정지를 시키는 방법
    • 우선 순위가 낮은 프로세스, 수행된 횟수가 적은 프로세스 등을 위주로 프로세스의 자원을 선점한다.

references : 자바의 정석 2판

www.daleseo.com/java-thread-runnable/

widevery.tistory.com/27

devbox.tistory.com/entry/Java-%EC%93%B0%EB%A0%88%EB%93%9C%EC%9D%98-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84

devbox.tistory.com/entry/Java-%EC%93%B0%EB%A0%88%EB%93%9C%EC%9D%98-%EB%8F%99%EA%B8%B0%ED%99%94

includestdio.tistory.com/12

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기