[Java] 스레드와 멀티스레드 프로그래밍

2021. 12. 4. 18:48Backend/☕️ Java

 

목차

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

 

 

짚고 가야할 개념

프로세스

프로세스는 프로그램이 실행되어 메인 메모리(RAM) 에 로드된 상태를 일컫는 말로, OS 입장에서는 관리해야할 하나의 작업 단위입니다.

 

스레드

OS 가 관리하는 작업 단위인 프로세스 내부에서(작업공간) 실제로 작업을 처리하는 단위(일꾼)입니다.

프로세스는 메인 메모리에 로드 된다고 하였는데, CPU 로 부터 메모리의 일정 부분을 할당받는 것이라 이해해도 좋습니다. 스레드는 이 할당받은 메모리를 자원으로 삼아 작업을 수행하며 모든 프로세스는 최소 하나 이상의 스레드가 존재한다.

 

멀티스레딩이란

하나의 프로세스에서 여러 스레드가 동시에 작업을 수행하는 것입니다.

CPU의 코어는 한 번에 하나의 작업을 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 일치합니다.

하지만 대부분 스레드의 수는 코어의 수보다 많기에, 각 코어가 아주 짧은 시간동안 여러 작업을 번갈아가며 수행함으로 여러 작업이 동시 수행되는 것 처럼 보입니다.

 

멀티 스레딩의 장점

CPU 사용율을 향상시켜 자원을 보다 효율적으로 사용할 수 있습니다. (정확히 말하면, CPU 사용율을 높이기 위해 멀티스레딩을 사용하는 것입니다.)

 

 

Thread 클래스와 Runnable 인터페이스

자바에서 스레드는 무엇인가요?

스레드는 프로그램 내부에서 동작하는 작업의 단위입니다. 즉 프로그램을 작동시키는 일꾼이라 생각할 수 있습니다. 자바에서 이 일꾼을 직접 생성하고, 컨트롤하는 방법이 2가지가 있습니다.

Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하면 됩니다. 이 둘은 공통적으로 run() 메소드를 가지고 있습니다. (왜냐하면 Thread 클래스는 Runnable 인터페이스를 상속받기 때문이죵)  이 run() 메소드 내부에 스레드가 할 일을 직접 코드로 구현할 수 있습니다.

run() 메소드만 선언돼 있는 Runnable 인터페이스
Runnable 인터페이스를 상속받는 Thread 클래스

 

그럼 Thread class 와 Runnable interface 의 차이는 무엇일까요?

 

하나는 클래스를 상속받는 것이고, 다른 하나는 인터페이스를 구현하는 것이네요. 스레드를 만드는데 왜 전혀 다른 2가지 방법이 존재하는 걸까요?

우선 자바는 클래스의 다중상속이 불가능합니다. 다중상속이 불가능 하다는 것은 한번에 두개 이상의 클래스를 상속받을 수 없음을 의미합니다. 만약 A라는 클래스가 이미 B 클래스를 상속받는 상황에서 스레드를 생성하고 싶다면 어떻게 해야할까요? Thread 클래스를 상속받는 것은 선택지에서 제거되겠네요. 그럼 나머지 하나의 선택지인 Runnable 인터페이스를 상속받아 구현하면 됩니다. 왜냐하면 인터페이스는 클래스와 달리 동시에 여러개를 상속(구현)할 수 있기 때문입니다.

스레드를 생성하는 방법이 왜 2가지 인가에 대한 답은 해결된 것 같네요.

 

그럼 두 방법 중 어느 것을 사용하면 되나요?

이미 클래스를 상속 받은 경우엔 Runnable 인터페이스를 구현하고, 그렇지 않을 땐 Thread 클래스를 상속받으면 됩니다. 그냥 간단하게 늘 Runnable 인터페이스를 구현하면 안될까요? 잠시 코드를 보면서 Thread 클래스를 상속받는 것 이 더 편리한 이유를 알아보겠습니다.

과 차이를 확인해보겠습니다.

 

ThreadMain 클래스는 Thread 를 상속받고, RunnableMain 은 Runnable 인터페이스를 구현하며 두 클래스는 공통적으로 run() 메소드를 구현하고 있습니다. 이 run() 메소드는 Thread 의 start() 메소드를 통해 호출(run)됩니다. 자세한 내용은 뒤에서 더 다뤄봅시다.

 

ThreadExample 에서 thread1, thread2 라는 객체를 만들어 내지만, 자세히 보면 둘의 생성과정에는 미세한 차이가 존재합니다.

Runnable 인터페이스를 상속받은 RunnableMain의 객체(runnableMain) 는 Thread() 생성자의 매개변수로 들어가는 반면, Thread 클래스를 상속받은 ThreadMain 의 객체는 곧바로 start() 메소드를 호출합니다.

public class ThreadMain extends Thread {
    @Override
    public void run() {
        System.out.println("by Thread Class");
    }
}


public class RunnableMain implements Runnable {
    @Override
    public void run() {
        System.out.println("by Runnable Interface");
    }
}


public class ThreadExample {
    public static void main(String[] args) {
        RunnableMain runnableMain = new RunnableMain();

        Thread thread1 = new ThreadMain();
        Thread thread2 = new Thread(runnableMain);

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

즉 Thread 클래스를 확장하여 스레드를 실행하는 방법이 조금 더 간단하다는 점을 확인할 수 있습니다.

어느 방법이 더 낫다라고 따지기 보단, 주어진 상황에 적절한 방법을 택하는 것이 옳습니다.

 

 

 

 

 

 

스레드의 상태

위 코드에서 start() 메소드를 호출하면, 각 클래스에서 구현한 run() 메소드가 자동 호출되는 것을 확인했습니다.

여기서 알 수 있는 점은, 스레드를 만들었다고 해서 무조건 실행 되는 것이 아니라, start()처럼 특정 호출을 해주어야만 실행된다는 점 입니다. start() 명령으로 1) 스레드가 시작되면서 run()이 호출되고, 2) run() 메소드가 종료되면 해당 스레드는 소멸됩니다. 그럼 스레드의 생성부터 소멸까지의 전체 생애주기(라이프 사이클)를 알아보겠습니다. 

 

 

출처: https://www.baeldung.com/java-thread-lifecycle

짚고 넘어가야할 run() 과 start()

JVM 이 처음 클래스를 로드할 때 실행하는 메서드는 main() 메서드 입니다. (우리가 그토록 많이 써온 public static void main() 맞습니다)

main() 메서드 내부에서 start() 메서드를 호출하면서 '새로운' 스레드 실행에 필요한 '새로운' 호출 스택을 생성하여 run 메소드를 호출합니다. main 메서드를 실행하는 메인 스레드와 다른 별개의 스레드A(가칭)가 실행되는 것을 확인할 수 있습니다. 

run() 이 종료되면서 콜 스택을 빠져나가면, 콜 스택이 비워져 생성된 호출 스택도 소멸됩니다.

 

 

메인스레드와 스레드A, 두개의 서로 다른 스레드가 생성된다고 하였습니다.

별개의 스레드, 그렇다면 하나의 스레드에서 예외가 발생하면, 다른 스레드도 함께 종료될까요? 코드로 확인해보겠습니다.

 

아래 코드는 링크에서 참조했습니다.

public class ThreadExample {
    public static void main(String[] args) throws Exception {
        Thread1 th1 = new Thread1();
        th1.start();
        throw new Exception();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
            }
            System.out.println("th1 end");
        }
    }
}

 

main스레드에서 예외를 발생시켜도, th1 스레드는 "th1 end" 까지 잘 출력되는 것을 확인할 수 있었습니다. (궁금점: th1이 실행되면 해당 스레드가 종료된 후에 throw new Exception() 이 발생하는 것인가? 그건 아닌거 같은게 예외 발생 로그가 th1 end 보다 먼저 출력됐어)

 

다시 스레드의 라이프 사이클 얘기로 돌아가봅시다.

스레드의 상태는 크게 4가지로 나뉩니다. 시작과 종료, 실행 대기와 일시정지.

상태 상수 설명
객체 생성 NEW 스레드는 생성되었지만, 아직 시작하지 않은 단계. start() 메소드 호출 전 단계.
실행 대기 (실행) RUNNABLE 실행 가능한 상태이자 실행중인 상태.
일시 정지 BLOCKED 사용하려는 객체의 LOCK 이 풀릴 때 까지 기다리는 상태
WAITING wait(), join() 등의 메소드에 의해 스레드가 일시 정지된 상태. 하나의 스레드가 일시정지 됐다는 건, 다른 스레드가 실행중이라는 뜻이다. 실행중인 스레드가 알려줄 때(notify())까지 정지 상태를 유지한다.
TIMED_WAITING 특정 시간동안 기다리는 상태
종료 TERMINATED 실행을 마치고 종료된 상태. 스레드는 소멸된다.

 

1.  스레드를 생성하고 start() 를 호출하면 스레드가 곧 바로 실행되는 것은 아닙니다. 실행 대기열(queue) 에 들어가 자신의 차례가 될 때까지 기다립니다. 모든 스레드에는 우선순위가 있고, 그 우선순위에 따라 실행대기열에 들어갑니다. 그리하여 우선순위가 높은 스레드가 낮은 스레드보다 우선 실행되는 것이죠.

 

2. 우선순위에 따라 자기 차례가 되면 실행됩니다.

 

3. 실행 상태의 스레드는 세가지의 다른 상태로 전이 될 수 있습니다. join(), sleep(), wait() 메소드가 호출되면 일시정지 상태가 되고, yield() 메소드를 만나면 자신에게 할당된 실행시간을 다른 스레드에게 양보하면서 실행 대기 상태로 변합니다. stop() 메소드를 만나면 종료되구요.

 

4. 일시정지 된 스레드는 지정된 일시 정지 시간이 지나거나 interrupt(), notify(), resume() 메소드가 호출되면 다시 실행대기열에 저장됩니다. RUNNABLE 상태가 되는거죠.  그렇게 자신이 실행될 때까지 기다립니다.

5. 실행을 모두 마치거나 stop() 이 호출되면 해당 스레드는 소멸됩니다.

 

 

 

스레드의 우선순위

위 그림에서 스레드의 실행대기 상태 순서는, 스레드의 우선순위에 따라 달라진다고 했습니다. 모든 스레드는 우선순위(PRIORITY)라는 속성(멤버 변수) 를 가지는데, 우선 순위의 값에 따라 스레드의 실행순서가 달라집니다. 

그럼 우선순위를 지정하는 필드와 메소드를 알아보겠습니다.

스레드의 우선순위 범위는 1 ~ 10 으로 책정되며, 숫자가 높을수록 높은 우선순위를 갖게됩니다. 

 

스레드 우선순위 필드 (상수)

public final static int MIN_PRIORITY = 1
// 쓰레드 우선 순위의 최소값입니다 (가장 낮은 순위)

public final static int NORM_PRIORITY = 5
// 디폴트로 지정되는 우선순위 값입니다 (일반 스레드의 순위)

public final static int MAX_PRIORITY = 10
// 쓰레드 우선 순위의 최대값입니다 (가장 높은 순위)

만약 스레드의 우선순위를 정할 일이 있다면, 위의 상수를 사용할 것을 권장하지만 되도록이면 지정하지 않는 것이 좋습니다.()

 

스레드 우선순위 지정 메서드

setPriority(int PriorityNumber) // 매개변수 값으로 스레드 우선순위를 지정합니다.

getPriority() // 해당 스레드의 우선순위를 리턴합니다.

 

 

Main 스레드

모든 자바 어플리케이션은 main 스레드가 main() 메소드를 실행하면서 시작됩니다. main 스레드의 흐름 안에서 멀티 스레드 어플리케이션은 필요에 따라 작업 스레드를 만들어 병렬로 코드를 실행할 수 있게 됩니다. 싱글 스레드의 경우, 메인 스레드가 종료되면 프로세스도 종료되지만, 멀티 스레드는 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스가 계속 실행되게 됩니다. (스레드는 병렬적으로 작업하기 때문이라 생각하시면 됩니다.)

 

메인 스레드와 더불어 꼭 알아야 할 데몬 스레드(Daemon thread)가 있습니다.

 

 

데몬 스레드

메인 스레드의 작업을 돕는 보조 역할의 스레드로, 메인 스레드가 종료되면 데몬 스레드도 자동으로 강제 종료됩니다. 워드의 자동 저장 기능이나, 모니터링 스레드를 예로 들 수 있겠네요. 아래 코드를 보면서 왜 데몬 스레드가 앞서 언급한 두 예시가 될 수 있는지 확인해보겠습니다.

 

public class DaemonThread extends Thread{
    public void run() {
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        DaemonThread thread = new DaemonThread();
        thread.start();
    }
}

메인 스레드에서 DaemonThread 객체의 스레드를 실행시킵니다. 해당 스레드는 sleep() 메소드에 의해 곧바로 대기 상태가 되며, 별다른 동작이 없기 때문에 쭉 대기 상태를 유지합니다. 그 사이 메인 스레드는 이미 종료됩니다. 

아무런 메시지를 출력하지 않고 해당 프로그램은 끝나지도 않게 됩니다.

 

이제 해당 스레드를 데몬 스레드로 지정한 후 다시 실행해보겠습니다. setDaemon() 메소드를 이용해 데몬 스레드를 지정할 수 있습니다.

 

이렇게 되면 결과는...!

곧바로 종료 메시지를 출력한 것을 확인할 수 있습니다.

즉 DaemonThread 객체의 스레드는 무한정 대기상태이더라도, 메인 스레드가 종료됨에 따라 같이 종료되는 것을 확인할 수 있습니다. 

 

모니터링 스레드를 별도로 띄워 어떤 프로세스를 모니터링 중이라 가정하겠습니다. 메인 스레드가 종료되면 관련된 모니터링 스레드도 종료되어야 해당 프로세스가 종료되는데, 그렇지 않고 모니터링 스레드만 살아있다면 해당 프로세스는 영영 종료되지 않은 상태로 남아있게 됩니다. 모니터링 스레드가 데몬 스레드로 지정돼있다면, 메인 스레드가 종료됨에 따라 자동으로 종료될 것이므로 해당 프로세스 역시 안전히 종료될 수 있습니다.

 

 

자바의 동기화 

예금이 10만원인 통장에서 서로 다른 두 스레드가 각각 접근하여 10만원, 5만원을 입급했습니다. 하나의 계좌에 10만원과 5만원을 입금하면 원래는 25만원이 돼야 하지만, 최종 저장된 예금은 20만원이 되는 일이 생겼습니다. 예금 10만원이라는 (공유) 자원에 동시에 접근하다 보니 발생한 일입니다. 이런 문제를 방지하기 위해서, 예금을 저장하는 과정 만큼은 하나의 스레드만 접근할 수 있도록 다른 스레드의 접근을 막아야 하는데, 우리는 이걸 임계 구역 이라 부르기로 했어요.

임계구역 안에서는 다른 스레드가 접근하지 못하도록 막는 방법 중 LOCK 개념을 활용할 수 있습니다. 공유 자원을 수정하는 코드 영역을 임계구역으로 지정하여, 공유 자원이 가지고 있는 lock 을 획득한 하나의 스레드만 이 영역 내의 코드를 수행할 수 있습니다. lock 을 획득한 스레드가 작업을 마친 후 lock 을 반납해야만, 또 다른 스레드가 공유자원에 접근하여 lock 을 획득하고 작업을 수행할 수 있습니다. 

 

그렇다면 lock 은 코드상에서 어떻게 구현할 수 있을까요?

자바에서는 synchronized 라는 예약어로 공유 자원의 lock 을 걸 수 있습니다.  코드로 예시를 보겠습니다.

 

1. 메소드 자체를 임계구역으로 선언

메소드가 호출된 시점부터 lock 을 설정합니다. 메소드가 종료되면 lock 이 반환되어 다른 스레드가 접근할 수 있게 됩니다.

public class ThreadExample {
    int amount;
    
    public synchronized void plus(int value) {
        amount += value;
    }
}

 

2. 메소드 내부 특정 영역을 임계구역으로 지정

lock 을 걸고자 하는 객체를 우선 지정합니다. 해당 블럭 영역 안으로 들어가면 스레드는 지정된 객체의 lock 을 얻고, 블럭을 벗어나면 lock 을 반납합니다.

public class ThreadExample {
    int amount;

    public void minus(int value) {
        synchronized (this) {
            amount -= value;
        }
    }
}

 

 

wait() 와 notify()

스레드의 라이프 사이클 그림을 다시 가져와봤습니다. 지금 주목해야할 메소드는 wait() 와 notify() 입니다.

wait() 가 호출되면, 스레드의 상태는 WAITING 으로 변하고, notify() 를 호출하면 다시 RUNNABLE 상태로 돌아갑니다.

 

동기화 얘기를 하다 갑자기 wait(), notify()  메소드는 왜 나왔을까요?

동기화된 임계 구역의 코드가 수행되던 중, 더 이상 작업을 진행할 수 없는 상황이 온다고 가정해봅시다. 해당 스레드(A)가 락을 가지고 있기 때문에, 다른 스레드는 영영 접근하지 못하게 됩니다. 이때 임계구역 내의 스레드를 잠시 쉬게 할 수 있다면(wait()), 다른 스레드(B)가 공유 자원에 접근하여 작업을 수행할 수 있게 됩니다. 이제 B 스레드의 작업이 마친 후 다시 A 스레드를 불러와(notify()) 임계구역의 코드를 수행할 수 있게 한다면, 프로그램이 전체적으로 원활하게 진행 될 수 있습니다.

즉 wait() 메소드는 스레드가 락을 반납하고 WAITING 상태로 바꿔주며, notify() 메소드는 대기중인 스레드를 다시 RUNNABLE로 돌려 lock 을 얻을 수 있는 상태로 만들어 줍니다.

 

 

 

synchronized 사용시 주의점

동기화 개념은 공통된 자원에 여러 스레드가 접근할 때 발생하는 문제를 막기 위해 도입된 개념입니다. 주목할 포인트는 공통된 자원입니다.  여러 스레드에서 하나의 객체(자원)에 있는 인스턴스 변수를 동시에 처리할 때, 발생할 수 있는 문제를 해결할 수 있는 것이 synchroized 입니다.

만약 서로 다른 객체에 스레드가 접근하는 상황이라면, synchrozied 가 필요할까요? 이미 아시겠지만 정답은 그렇지 않습니다. 그렇기 때문에 아무런 메소드에 synchronized 를 남발하는 상황은 없어야 합니다.

 

 

 

데드락 ( DeadLock, 교착 상태 )

데드락, 교착 상태란?

학부 OS 수업때 많이 들은 단어입니다. 그 때 부터 OS의 중요성을 알았어야하는데

 

서로 다른 둘 이상의 프로세스가 다른 프로세스의 작업이 끝나기만 기다리며, 더 이상 작업을 진행하지 못하는 상태를 일컫습니다.

속된 말로 빼박이라 하죠. 프로세스가 데드락이 되는 4가지 조건이 존재하는데, 어떤 조건이길래 프로세스가 빼도박도 못하는지 알아보겠습니다.

 

교착 상태의 원인(조건)

아래 4가지 조건을 빠짐없이 모두 만족할 때 교착상태가 발생합니다. 다르게 말하면, 4가지 중 하나의 조건이라도 충족하지 않는다면 데드락에 걸리지 않습니다. 우선 4가지 조건을 알아본 후 교착 상태를 예방하는 법을 알아보겠습니다.

  1. 상호배제 : 한 자원에 여러 스레드가 동시에 접근 불가하기 때문에 교착 상태가 발생합니다. 남의 밥그릇은 탐하지 않는군요.
  2. 점유와 대기 : 자원을 가진 상태에서 다른 스레드가 사용중인 자원 반납을 기다립니다. 점유 하지 않은 상태에서 다른 자원을 기다린다면, 애초에 교착상태에 봉착하지 않았을지도 모르겠습니다.
  3. 비선점 : 다른 스레드가 선점한 자원을 중간에 뺐을 수 없습니다. 만약 스레드간 우선순위가 달랐다면 선점이 가능하여 데드락을 회피할 수 있었을 것입니다.  어느정도 상도덕을 지키다보니 교착상태에 빠졌네요. 
  4. 원형 대기: 자원을 요구하는 방향이 원형이기 때문에 교착상태가 발생합니다. 만약 일관된 방향으로 자원을 요구했다면 교착상태에 걸리지 않았을 것입니다.

 

교착 상태에 빠질 조건을 알아보았습니다. 그렇다면 이 교착 상태는 어떻게 방지하고 정상 상태로 돌릴 수 있을까요?

 

교착 상태 예방

교착 상태는 위의 4가지 조건을 모두 충족할 때 발생한다고 하였습니다. 그렇다면 이 중 하나의 조건이라도 사전에 막는다면 데드락을 방지할 수 있습니다. 하지만 이 방법은 실효성이 적어 잘 사용되지 않습니다.

 

교착 상태 회피

자원 할당량을 조절하여 교착 상태를 해결하는 방법입니다. 

 

교착 상태 검출과 회복

교착 상태 검출은 데드락을 미리 예방하는 대신, 자원 할당 그래프를 모니터링 하면서 교착 상태가 발생하는지 지켜보는 방식입니다. 교착상태가 발생하면 교착 상태 회복단계가 진행되며, 이 방법이 교착상태를 해결하는 현실적인 접근 방법입니다.

 

코드를 통해 데드락이 발생하는 경우를 보겠습니다. 출처는 이곳입니다.

public class MainThread {

    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

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

    }

    private static class FirstThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){
                System.out.println("First Thread has object1's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

    private static class SecondThread extends Thread{
        @Override
        public void run() {
            synchronized (object2){
                System.out.println("Second Thread has object2's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object1){
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}

메인 스레드가 종료되지 않은 것을 볼 수 있습니다. 교착상태에 걸렸다는 거죠. 내이름은 교착상태, 끝나지 않죠

데드락에 걸린 상태를 간단한 그림으로 확인해보겠습니다.

 

각 스레드는 서로 다른 객체에 접근합니다. 1) 상호 배제 조건을 충족시킵니다.

 

그런 다음 각 스레드는 synchronized 예약어를 사용하면서 해당 객체를 2) 점유 대기 하게됩니다. 그러다 다른 객체를 점유하기 위해 또 synchronzied 를 사용합니다. 이때 서로가 점유중인 객체를 요구하게 되기 때문에 3) 원형 대기 조건도 만족하게 됩니다. 또한 각 스레드는 우선순위를 따로 지정하지 않았기 때문에 NORM_PRIORITY 를 가지며 4) 비선점 상태 역시 만족하며 데드락의 모든 조건을 충족하게 됩니다.

 

 

 

데드락의 4가지 조건중 하나라도 불충족시킨다면 교착상태를 빠져나올 수 있습니다. 위 코드에서 조금만 변형하여 아래 코드를 실행해보겠습니다.

public class MainThread {

    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

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

    }

    private static class FirstThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){
                System.out.println("First Thread has object1's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

    private static class SecondThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){  // 달라진 부분 (object2 -> object1)
                System.out.println("Second Thread has object2's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object2){ // 달라진 부분 (object1 -> object2)
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}

 

 

 

마무리

자바 스레드의 전반적인 개념을 알아봤습니다. 스레드를 생성할 수 있는 Thread 클래스와 Runnable 인터페이스를 시작으로, 스레드가 가지는 4가지 상태(객체 생성, 실행 대기, 일시 정지, 종료), 실행 대기중인 스레드 중 어느것이 먼저 실행될지 정할 수 있는 스레드 우선순위 를 먼저 알아보았습니다. 여러 스레드가 하나의 공유 객체에 접근할 때 발생할 문제를 막기 위해 동기화 과정과 데드락 까지 살펴보면서 멀티 스레딩 환경에서 어떻게 대처를 해야하는지 알아볼 수 있었습니다. 긴글 읽어주셔서 감사합니다. 피드백은 언제나 환영입니다.

 

ref