코딩하는 털보

STUDY HALLE - 10주차 본문

Diary/Study Halle

STUDY HALLE - 10주차

이정인 2021. 1. 22. 22:27

STUDY HALLE

10주차 : 멀티쓰레드 프로그래밍


목표

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

학습할 것

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

멀티쓰레드 프로그래밍 ?

Thread ?

예전에는 프로그램을 실행하는 흐름이 오로지 프로세스뿐이었으나, 소프트웨어가 진보하면서 하나의 프로그램에서 복잡한 동시 작업을 요구하기 시작하였다. 이를 위해서는 하나의 프로그램이 여러개의 프로세스를 만들어야 했는데 프로세스 특성상 하나의 프로그램이 이러한 동시 작업을 수월하게 할 수가 없었다.

그래서 프로세스보다 더 작은 실행 단위 개념이 만들어지게 되는데 이것이 쓰레드이다. 하나의 프로세스에서 여러개의 쓰레드가 메모리를 공유하여 작동할 수 있으며, 그래서 생성과 속도가 빠르고, 적은 메모리를 점유하며, 정보 교환이 쉽고 Context Switching 부하가 적지만 그 대가로 자원 선점과 동기화 문제를 얻게 되었다. 대다수 OS의 스케줄러는 쓰레드를 최소 단위로 하여 작동한다.

CPU 사양 상에서 4코어 8쓰레드 등으로 언급되는 쓰레드는 위에서 언급한 것과 같으나 단위로서의 뉘앙스가 더 강하다. 4개의 코어로 최대 8개의 작업을 동시 처리할 수 있다든지. 일반적으로 하나의 코어는 한번에 하나씩의 작업만 처리할 수 있지만 SMT를 통해 하나의 코어가 어느정도 다중 처리 능력을 가지게 할 수 있으며 이럴 경우 물리적 코어 개수와 처리 가능한 쓰레드의 숫자가 다르게 된다.

참고 : 나무위키 - 스레드

정리) Thread is ...

  • 현대 프로그램을 실행하는 최소의 실행 단위
  • 하나의 프로세스에서 하나 이상의 쓰레드를 가지게 됨
  • 장점 : 프로세스 내에서 메모리 공유로 빠른 속도, 적은 Context Switching 부하
  • 단점 : 자원 선점 및 동기화 문제

Context Switching ?

하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(Context)를 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다. 한 프로세스의 Context는 그 프로세스의 프로세스 제어 블록(PCB)에 기록되어 있다.

위키백과 - 문맥 교환

PCB ?

특정한 프로세스를 관리할 필요가 있는 정보를 포함하는 운영 체제 커널의 자료 구조이다. 작업 제어 블록(Task Control Block, 줄여서 TCB) 또는 작업 구조라고도 한다.

위키백과 - 프로세스 제어 블록

멀티쓰레드 프로그래밍

여러 쓰레드에서 task를 나누어 프로그램을 처리하도록 구현하는 것을 멀티쓰레드 프로그래밍이라고 한다.

우리는 이 다음에서 자바에서의 멀티쓰레드 프로그래밍 방법을 알아볼 것이다.


Thread 클래스와 Runnable 인터페이스

자바에서의 멀티쓰레드 프로그래밍

자바 멀티쓰레드 프로그래밍을 위해 우리는 자바의 Thread 클래스와 Runnable 인터페이스를 사용한다.

동일한 메인 쓰레드에서 생성된 모든 쓰레드는 서로 생성하는 인스턴스를 공유한다는 특징이 있다.

Thread 클래스 사용

Thread 클래스를 상속받는 쓰레드 클래스를 만들기

class MyThread extends Thread {

    //run() 메소드에 이 Thread 클래스의 동작을 구현하여 오버라이딩 해주어야 한다.
    @Override
    public void run() {
        int i;
        for (i=0;i<20;i++) {
            System.out.print(i + "\t");
            try {
                sleep(5000); //Thread 클래스의 static 메소드이다. 해당 milli초 만큼 대기한다.
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

Thread 객체를 생성하고 실행시키기

public class ThreadTest {
    public static void main(String[] args) {

        System.out.println("start");
        Thread th1 = new MyThread();
        Thread th2 = new MyThread();

        th1.start(); //Thread를 실행시킨다.
        th2.start();
        System.out.println("end");
    }
}

output

start
end
0    0    1    1    2    2    3    3    4    4    5    5    6    6    7    7    8    8    9    9    
Process finished with exit code 0

출력 결과에서 확인할 수 있는 단일 쓰레드 프로그램과 비교해봤을 때 특이한 점은?

구현부에서는 System.out.println("end");start() 메소드보다 아래에 있지만 먼저 실행된 것!

메인 쓰레드가 생성한 쓰레드의 task를 지시만 할 뿐 종료까지 기다리지 않고 바로 "end"를 출력해버렸기 때문이다.

생성된 쓰레드들 서로끼리도 마찬가지이다. th1이 먼저 실행되었다고해서 th2가 th1의 모든 처리가 종료될 때 까지 기다리지는 않았다. th1과 th2가 동시에 출력을 실행하고 있는 것이다.

멀티쓰레드 프로그래밍은 예제처럼 메인 쓰레드와 생성된 쓰레드들이 모두 동시에 프로그램을 처리할 수 있도록 해준다.

Runnable 인터페이스 사용

Runnable 인터페이스를 구현하는 쓰레드 클래스를 만들기

class MyRunnable implements Runnable {

    //Thread 클래스를 상속받아 구현하는 것과 동일하게 run() 메소드 오버라이딩
    @Override
    public void run() {
        int i;
        for (i=0;i<100;i++) {
            System.out.print(i + "\t");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

Runnable 인터페이스는 단 한 개의 추상메소드만 가지고 있는 함수형 인터페이스이다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

함수형 인터페이스는 자바 람다식에 사용되는데, 람다식은 15주차 주제이므로 킵...

아무튼, run() 메소드 하나만 구현하면 되므로 Thread 클래스를 상속받아 구현하는 것과 크게 다른 점이 없다.

Thread 클래스와 다른 점은 실행하는 부분에 있다.

Thread 객체를 생성하고 실행시키기

public class RunnableTest {
    public static void main(String[] args) {

        System.out.println("start");

        //Runnable 인터페이스를 구현한 객체를 생성한다.
        MyRunnable myTh1 = new MyRunnable();
        MyRunnable MyTh2 = new MyRunnable();

        //Runnable 타입 객체를 Thread 객체 생성자 파라미터로 받을 수 있다.
        Thread th1 = new Thread(myTh1);
        Thread th2 = new Thread(MyTh2);

        th1.start();
        th2.start();
        System.out.println("end");
    }
}

Thread 클래스를 상속받은 클래스를 사용한 경우에는 해당 클래스의 객체에서 바로 start() 메소드를 사용하여 실행할 수 있지만,

Runnable 인터페이스를 구현한 클래스는 객체 생성 후 Runnable 타입 인자를 받는 생성자를 통해 별도의 Thread 객체를 생성한 뒤에야 start() 메소드를 호출하여 실행할 수 있다.

그럼에도 불구하고 Runnable 인터페이스를 사용하는것이 더 바람직한데, 그 이유는 자바에서는 다중 상속을 받을 수 없으므로 Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문이다.

output 결과는 이전과 동일하다.

start
end
0    0    1    1    2    2    3    3    4    4    5    5    6    6    7    7    8    8    9    9    
Process finished with exit code 0

쓰레드의 상태

ThreadStateDiagram

사진 출처 : bitstechnotes - ThreadStateDiagram

Thread.State

Thread.State로 확인할 수 있는 쓰레드의 상태 값은 아래와 같다.

  • NEW
    생성되었지만 아직 시작(start())되지 않은 thread 상태
  • RUNNABLE
    start() 메소드 호출로 실행 가능 상태에 있는 thread 상태, 쓰레드가 실제로 실행 중이거나 언제든지 실행할 준비가 되어있는 상태인데, 이것은 쓰레드 스케쥴러에 의하여 결정된다.
  • BLOCKED
    실행 중 다른 스레드에 의해 잠겨있는 영역에 접근하려고 하여 차단 된 thread 상태
  • WAITING
    실행 중 다른 thread가 특정 액션(I/O 등)을 실행하는 것을 무기한으로 대기하고 있는 thread 상태
  • TIMED_WAITING
    실행 중 지정된 대기 시간 동안 다른 thread가 액션을 실행하는 것을 대기하고 있는 thread 상태
  • TERMINATED
    종료된 thread 상태

Thread Scheduler ?

Context Switching이 발생할 때 그 다음 어떤 Runnable 쓰레드를 실행할 지 결정해주는 역할을 한다.

여러 Runnable 쓰레드들을 번갈아가면서 실행하여 흡사 동시에 실행되는 것처럼 동작하도록 해준다.

참고 :

Oracle Java Docs - Thread.State

https://www.javatpoint.com/thread-scheduler-in-java

https://www.geeksforgeeks.org/lifecycle-and-states-of-a-thread-in-java/

확인해보기

import static java.lang.Thread.sleep;

class MyThread implements Runnable {
    public void run() {
        int i;
        for (i=0;i<5;i++) {
            System.out.println(i + "\t");
            try {
                sleep(5000); //Thread 클래스의 메서드
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        for(i=0; i<100000; i++){
            for(int a=0; a<10000; a++){
                for(int b=0;b<10000;b++){}
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {

        System.out.println("start");
        MyThread myTh1 = new MyThread();
        Thread th1 = new Thread(myTh1);

        System.out.println("===============");
        System.out.println(th1.getState());
        th1.start();
        sleep(1000);
        System.out.println("===============");
        System.out.println(th1.getState());
        sleep(24100);
        System.out.println("===============");
        System.out.println(th1.getState());
        sleep(5000);
        System.out.println("===============");
        System.out.println(th1.getState());
        System.out.println("end");
    }
}
start
===============
NEW
0    
===============
TIMED_WAITING
1    
2    
3    
4    
===============
RUNNABLE
===============
TERMINATED
end

프로그램 중간중간의 Thread.State 값은 위와 같다.

공부한 것 처럼...

  • Thread 객체 생성 후 start() 호출하기 전까지는 NEW.

  • 정해진 시간을 대기하는 sleep(5000) 중에는 TIMED_WAITING.

  • 실행중(3단계 for문)에는 RUNNABLE.

  • Thread가 모든 task를 마친 뒤에는 TERMINATED.


쓰레드의 우선순위

멀티쓰레딩 환경에서 쓰레드 스케줄러는 우선 순위에 따라 쓰레드를 실행한다.

쓰레드를 만들 때마다 항상 우선 순위가 지정되는데, 우선 순위는 JVM에 의해 주어지거나 프로그래머가 명시 적으로 지정할 수 있다.

  • Runnable 중에서 제일 우선순위가 높은 쓰레드를 먼저 실행한다.

  • 우선순위에 사용되는 값은 숫자 1에서 10까지의 수이다.

  • 우선순위를 위해 기본적으로 제공되는 3가지 static 변수가 있다.

    • MIN_PRIORITY : 최저 우선순위 값 (=1)
    • NORM_PRIORITY : 기본 우선순위 값 (=5)
    • MAX_PRIORITY : 최고 우선순위 값 (=10)

우선순위 지정하기

class MyRunnable implements Runnable {

    int num;

    public MyRunnable(int num) {
        this.num = num;
    }

    public void run() {
        for(int i=0; i<100000; i++){
            for(int a=0; a<10000; a++){
                for(int b=0;b<100000;b++){}
            }
        }
        System.out.println(num+"번 쓰레드 종료");
    }
}

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {

        System.out.println("start");
        MyRunnable myTh1 = new MyRunnable(1);
        Thread th1 = new Thread(myTh1);
        MyRunnable myTh2 = new MyRunnable(2);
        Thread th2 = new Thread(myTh2);
        MyRunnable myTh3 = new MyRunnable(3);
        Thread th3 = new Thread(myTh3);

        th1.setPriority(1);
        th3.setPriority(10);

        System.out.println("1 thread priority : "+th1.getPriority());
        System.out.println("2 thread priority : "+th2.getPriority());
        System.out.println("3 thread priority : "+th3.getPriority());

        Thread.currentThread().setPriority(10);
        System.out.println("main thread priority : "+Thread.currentThread().getPriority());

        MyRunnable myTh4 = new MyRunnable(4);
        Thread th4 = new Thread(myTh3);

        System.out.println("4 thread priority : "+th4.getPriority());

    }
}

Thread 클래스의 setPriority(int newPriority) 메소드를 통해 우선순위를 지정하고 getPriority() 메소드를 통해 현재 우선순위를 확인할 수 있다.

output

start
1 thread priority : 1
2 thread priority : 5
3 thread priority : 10
main thread priority : 10
4 thread priority : 10

주목할 것은 th2와 th4 두 쓰레드 둘 다 기본 우선순위를 사용했는데도 불구하고 th2의 우선순위는 5, th4의 우선순위는 10이 되었다. 이 두 쓰레드의 기본 우선순위 값이 다른 이유는 무엇일까?

이것은 중간에 main 쓰레드의 우선순위가 변경되었기 때문인데, main 쓰레드의 기본 우선 순위는 5이지만 10으로 변경된 후 생성된 하위 쓰레드(th4)가 main 쓰레드의 변경된 우선순위(10)를 기본 우선 순위로 사용하게 된 것이다.

일반적으로 다른 모든 스레드의 기본 우선 순위는 상위 스레드의 우선 순위를 따라간다.

https://www.geeksforgeeks.org/java-thread-priority-multithreading/


Main 쓰레드

멀티쓰레드 프로그램에서는 여러 쓰레드가 필요하지만, 그것과 별개로 어느 프로그램이든지 적어도 한 개 이상의 쓰레드는 task를 처리해야 한다.

위 필요사항에 따라 JVM이 자바 프로그램에 기본적으로 제공하는 쓰레드가 main 쓰레드이다.

main 쓰레드의 중요 포인트

  • JVM은 자바 어플리케이션을 실행할 때 main 쓰레드를 생성하여 main() 메소드를 호출한다.

  • 다른 자식 쓰레드는 모두 main 쓰레드에서 생성된다.

  • 실행을 완료하고 자바 어플리케이션을 종료하는 마지막 쓰레드가 되어야한다.

    main 쓰레드가 먼저 중지되는 경우 프로그램 실행이 종료된다.

참고 : https://javagoal.com/main-thread-in-java/#1


동기화

임계 영역(critical section)

  • 두 개 이상의 thread가 동시에 접근하게 되는 리소스
  • critical section에 동시에 thread가 접근하게 되면 실행 결과를 보장할 수 없다.
  • thread간의 순서를 맞추는 동기화(synchronization)가 필요하다.

동기화

  • 임계 영역에 여러 thread가 접근하는 경우, 한 thread가 수행하는 동안 공유 자원을 lock하여 다른 thread의 접근을 막는 것.

  • 동기화를 잘못 구현하면 deadlock에 빠질 수 있다.

자바에서의 동기화 구현

synchronized 블록, synchronized 메소드 또는 wait()-notify() 메소드를 사용하여 구현한다.

synchronized 블록

쓰레드가 독점적으로 실행해야하는 부분을 표시한다.

synchronized(참조형 수식) {
} //참조형 수식에 해당되는 객체에 lock을 건다

5초동안(ㅋ) 자전거를 빌려주는 서비스, 우리 동네에는 아쉽게도 이 서비스의 자전거가 하나뿐이다.

두 명이 이 자전거(임계 영역)를 빌리려고 할 때, 당연히 늦은 사람은 먼저 빌린 사람이 다 탈 때 까지 대기할 필요가 있다.

또한 각자의 사용량이 잘 반영되어야지 자전거 베터리가 얼마나 남았는지 알 수 있다.

만약 동기화를 사용하지 않는다면 서로의 자전거 이용은 무시하며 동시에 예약이 되고 베터리 수명도 잘 기록되지 않는다.

동기화를 사용하여 자전거를 한 쓰레드가 독점하도록 해보자.

package me.rockintuna.demospringdata;

import static java.lang.Thread.sleep;
import static me.rockintuna.demospringdata.BicycleRental.bicycle;

class Bicycle {

    int chargeRate = 100;
    public void rental() throws InterruptedException {
        synchronized(this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }
}

class Member extends Thread {
    @Override
    public void run() {
        try {
            bicycle.rental();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class BicycleRental {

    public static Bicycle bicycle = new Bicycle();

    public static void main(String[] args) throws InterruptedException {

        Member th1 = new Member();
        Member th2 = new Member();

        th1.start();
        th2.start();
        sleep(2000);
        System.out.println(th2.getState());

    }
}

output

남은 충전량은 100%
자전거 예약합니다.
BLOCKED
자전거 반납합니다.
남은 충전량은 70%
=================
남은 충전량은 70%
자전거 예약합니다.
자전거 반납합니다.
남은 충전량은 40%
=================

대기하는 중에 동일한 임계 영역(자전거)에 접근하려고 했던 th2의 쓰레드 상태는 BLOCKED가 되었다.

synchronized 메서드

현재 이 메소드를 가지고 있는 객체에 lock을 건다.
deadlock 방지를 위해 synchronized 메서드 내에서 다른 synchronized 메서드를 호출하지 않아야 한다.

package me.rockintuna.demospringdata;

import static java.lang.Thread.sleep;
import static me.rockintuna.demospringdata.BicycleRental.bicycle;

class Bicycle {

    int chargeRate = 100;
    public synchronized void rental() throws InterruptedException {
        System.out.println("남은 충전량은 " + this.chargeRate + "%");
        System.out.println("자전거 예약합니다.");
        sleep(5000);
        this.chargeRate -= 30;
        System.out.println("자전거 반납합니다.");
        System.out.println("남은 충전량은 " + this.chargeRate + "%");
        System.out.println("=================");
    }
}

class Member extends Thread {
    @Override
    public void run() {
        try {
            bicycle.rental();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class BicycleRental {

    public static Bicycle bicycle = new Bicycle();

    public static void main(String[] args) throws InterruptedException {

        Member th1 = new Member();
        Member th2 = new Member();

        th1.start();
        th2.start();

    }
}

output

남은 충전량은 100%
자전거 예약합니다.
자전거 반납합니다.
남은 충전량은 70%
=================
남은 충전량은 70%
자전거 예약합니다.
자전거 반납합니다.
남은 충전량은 40%
=================

wait()-notify() 메소드

이전 상황에서는 한 쓰레드가 종료될 때 까지 다음 쓰레드는 대기하는 로직이었으나, 만약 종료되기 전에 임계 영역 접근을 해제해야할 필요가 있을 때는? wait()-notify() 메소드의 사용이 필요하다.

wait()-notify() 메소드는 synchronized 블록 또는 메소드 내에서 사용할 수 있으며, 임계 영역에 대한 lock과 쓰레드 상태를 보다 구체적으로 조작할 수 있도록 도와준다.

wait() : 리소스가 더 이상 유효하지 않은 경우 리소스가 사용 가능할 때 까지 기다리기 위해 thread를 WAITING 상태로 전환시킨다. wait() 상태가 된 thread는 notify()가 호출 될 때까지 대기한다

notify() : wait()하고 있는 thread중 한 thread를 Runnable 상태로 변경

nofifyAll() : wait()하고 있는 모든 thread를 Runnable 상태로 변경

자전거 서비스가 유행해서 우리 지역에 3대의 자전거로 늘어났다.

6명이 동시에 자전거 대여를 하려고 하는데... 지난번 코드로는 한명이 예약하면 자전거가 남았음에도 불구하고 자전거 모두를 점유하기 때문에 나머지 5명이 기다려야하는 불상사가 일어난다.

wait()를 이용해서 자전거가 없을 때 대기하도록 하고, 자전거를 반환하면 notifyAll()로 WAITING 상태의 쓰레드를 깨우도록 해보자.

import java.util.ArrayList;
import java.util.List;

import static java.lang.Thread.sleep;
import static me.rockintuna.demospringdata.BicycleRental.bicycle;

class Bicycle {

    public List<String> bicycles = new ArrayList<>();

    public Bicycle() {
        bicycles.add("자전거1");
        bicycles.add("자전거2");
        bicycles.add("자전거3");
    }

    public synchronized String rentalBicycle(String name) throws InterruptedException {

        if (bicycles.size() == 0) {
            wait();
        }
        String bic = bicycles.remove(0);
        System.out.println(name+" : "+bic+" 예약합니다.");
        return bic;

    }

    public synchronized void returnBicycle(String name, String bic) {
        bicycles.add(bic);
        notifyAll();
        System.out.println(name+" : "+bic+" 반납합니다.");
    }
}

class Member extends Thread {
    @Override
    public void run() {
        try {
            String bic = bicycle.rentalBicycle(Thread.currentThread().getName());
            sleep(5000);
            bicycle.returnBicycle(Thread.currentThread().getName(), bic);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class BicycleRental {

    public static Bicycle bicycle = new Bicycle();

    public static void main(String[] args) {

        Member th1 = new Member();
        Member th2 = new Member();
        Member th3 = new Member();
        Member th4 = new Member();
        Member th5 = new Member();
        Member th6 = new Member();

        th1.start();
        th2.start();
        th3.start();
        th4.start();
        th5.start();
        th6.start();

        sleep(1000);
        System.out.println(th4.getState());

    }
}
Thread-0 : 자전거1 예약합니다.
Thread-1 : 자전거2 예약합니다.
Thread-2 : 자전거3 예약합니다.
WAITING
Thread-1 : 자전거2 반납합니다.
Thread-0 : 자전거1 반납합니다.
Thread-2 : 자전거3 반납합니다.
Thread-5 : 자전거2 예약합니다.
Thread-4 : 자전거1 예약합니다.
Thread-3 : 자전거3 예약합니다.
Thread-4 : 자전거1 반납합니다.
Thread-3 : 자전거3 반납합니다.
Thread-5 : 자전거2 반납합니다.

동시에 3명이 대여할 수 있고 반납과 동시에 기다리던 인원이 대여한다.

wait()로 인해 대기하고 있던 th4는 WAITING 상태였다.


데드락

두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태이다.

특히, 자바 멀티쓰레드 프로그래밍에서는 둘 이상의 쓰레드가 서로 다른 쓰레드가 점유한 락의 점유가 풀리기만을 기다리는 무한정 대기상태이다.

ex)

Thread A는 'a' 객체를 점유한 상태로 'b' 객체를 요청하는 동시에

Thread B는 'b' 객체를 점유한 상태로 'a' 객체를 요청한다면?

Thread A, B 둘 다 서로가 점유한 객체를 무한정 기다리는 데드락 상태가 된다.

참고 :

https://ko.wikipedia.org/wiki/%EA%B5%90%EC%B0%A9_%EC%83%81%ED%83%9C

데드락 시뮬레이션

import static java.lang.Thread.sleep;
import static me.rockintuna.demospringdata.BicycleRental.bicycle;
import static me.rockintuna.demospringdata.BicycleRental.car;

class Car {
    int chargeRate = 100;
    public void rental(Bicycle bicycle) throws InterruptedException {
        synchronized(this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자동차 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;

            //반납하기 전에 자동차 대여 시도
            synchronized (bicycle) {
                bicycle.rental(car);
            }
            System.out.println("자동차 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }
}

class Bicycle {

    int chargeRate = 100;

    public void rental(Car car) throws InterruptedException {
        synchronized (this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            synchronized (car) {
                car.rental(bicycle);
            }
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }
}

class Member extends Thread {
    @Override
    public void run() {
        try {
            bicycle.rental(car);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Member2 extends Thread {
    @Override
    public void run() {
        try {
            car.rental(bicycle);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class BicycleRental {

    public static Bicycle bicycle = new Bicycle();
    public static Car car = new Car();

    public static void main(String[] args) throws InterruptedException {

        Member th1 = new Member();
        Member2 th2 = new Member2();

        th1.start();
        th2.start();

        sleep(6000);
        System.out.println(th1.getState());
        System.out.println(th2.getState());

    }
}

th1은 자전거 대여 후 반납하기 전에 자동차 대여를 시도하고 th2는 자동차 대여 후 반납하기 전에 자전거 대여를 시도하는 로직이다.

th1은 th2가 자동차를 반납하기를 기다리고, th2는 th1이 자전거를 반납하길 기다리기만 하는 교착 상태에 빠지게 된다.

output

남은 충전량은 100%
자전거 예약합니다.
남은 충전량은 100%
자동차 예약합니다.
BLOCKED
BLOCKED

두 쓰레드 모두 BLOCKED 상태에서 더 이상 진행되지 않는다.

데드락을 방지하는 방법

  • 한 번에 여러 락을 잡지 않음

    동기화 블럭에 동기화 블럭 포함시키지 않는 코드는 다음 동기화 작업을 할 때 현재의 모든 락의 점유를 반납한 뒤에 수행하기 때문에 동시에 여러 락을 잡지 않으며 데드락이 발생하지 않는다.

자전거와 자동차를 연속으로 사용하는 사람은 먼저 사용한 이동 수단을 꼭 반납해야 한다.

class Car {
    int chargeRate = 100;

    public void rental() throws InterruptedException {
        synchronized(this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자동차 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자동차 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }

    public void rental(Bicycle bicycle) throws InterruptedException {
        synchronized(this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자동차 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자동차 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
        //동기화 코드 밖에서 다음 동기화 작업 실행
        bicycle.rental();
    }
}

class Bicycle {
    int chargeRate = 100;

    public void rental() throws InterruptedException {
        synchronized (this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }

    public void rental(Car car) throws InterruptedException {
        synchronized (this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
        car.rental();
    }
}
  • 락 정렬

    데드락은 서로 같은 락을 점유하려하는데 서로 다른 순서로 점유하려 할 때 발생하게 되므로, 락의 점유 순서를 고정하여 데드락을 회피할 수 있다.

    락의 점유 순서가 고정될만한 논리적인 근거가 있어야하기 때문에 언제나 가능한 회피 방법은 아니다.

자전거와 자동차를 연속 사용하려면 자전거 먼저 사용하도록한다.

또는 자동차 반납을 강제한 뒤 자전거 사용을 허가한다.

class Car {
    int chargeRate = 100;
    public void rental() throws InterruptedException {
        synchronized(this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자동차 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자동차 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }
}

class Bicycle {

    int chargeRate = 100;
    public void rental() throws InterruptedException {
        synchronized (this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }

    public void rental(Car car) throws InterruptedException {
        synchronized (this) {
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("자전거 예약합니다.");
            sleep(5000);
            this.chargeRate -= 30;
            synchronized (car) {
                car.rental();
            }
            System.out.println("자전거 반납합니다.");
            System.out.println("남은 충전량은 " + this.chargeRate + "%");
            System.out.println("=================");
        }
    }
}

자동차를 점유하는 쓰레드가 더이상 또 다른 락을 점유하려고 하지 않기 때문에 데드락이 발생할 수 없다.

  • 락 타임 아웃

    쓰레드가 락을 위해서 기다리는 시간을 제한할 수 있다.

    사실 타임아웃은 데드락은 발생하지만 데드락에 걸린 후에 완전 대기 상태를 빠져나오는 방법이다.

    대기상태를 빠져나온 후에 재시도하여 다시 락 점유를 시도할 수 있다.

    다만, 타임아웃은 데드락에 대해서만 발생하지 않는다. 락을 점유하고 일반적인 프로그램 절차를 진행하고 있는 쓰레드를 기다리고 있는 다른 쓰레드에서도 타임아웃은 발생할 수 있다.

Comments