개발은 아름다워

[ Java ] 동기화 비동기화 블로킹 논블로킹 외우지말고 이해하자 본문

자바

[ Java ] 동기화 비동기화 블로킹 논블로킹 외우지말고 이해하자

do_it_zero 2024. 10. 30. 10:14

그냥 외우는면 되는게 아니야

부트캠프에 들어가서 교육 받고 있을때였다. 

당시 부트캠프에서는 면접 질문 300제라는게 있었는데, 면접에 나올 질문들에 대한 답이 있는 파일이였다

300제 중 동기화 비동기화 블로킹과 논블로킹에 대한 면접 질문과 답이 있었는데, 그저 면접 대비를 해야한다길래 이해 없이 그냥 외웠던 기억이 있다.

이해가 안되니 심지어 외우는 것도 어려웠다...

이해 없이 어렵게 외우려고만 하니 동기화,비동기화 이런 얘기만 들려도 덜썩 겁부터 났다.

 

이제와서 돌이켜보니,,,

프로그래밍 공부에서 이해 없이 단순히 외웠던 지식들은 솔직히 하나도 쓸모가 없다...

단순히 외웠던 개념들은 코드에 어떻게 적용되어 있는지도 모르니 도움이 되는게 없었다.

오히려 어렵게 외웠던 단어들에 대한 두려움만 생길 뿐이였다.

 

그래서 오늘은 어렵게 외우기만 했던 동기화와 비동기화 블로킹과 논블로킹에 대해 정리하려고 한다.

 

왜 헷갈리고 어렵다고 느껴졌을까?

동기화 작업이면 다른 스레드의 접근을 못하게 한다. 다른 스레드는 작업을 진행하지 못한다.

블로킹은 스레드의 작업을 멈추게한다. 해당 스레드는 작업을 진행하지 못한다.

 

헷갈렸던 점은 동기화든 블로킹이든 둘다 작업을 진행하지 못한다는 것 자체였다.

 

그렇다면 왜 작업을 진행하지 못하게 되는걸까? 이 부분에 대한 정확한 이해를 하지 못했었다.

이 부분을 이해하기 위해서는 관점에 차이가 있다는 것을 이해해야 한다.

 

멀티스레드 상황에서 동시 작업 가능하게 할래 말래?

자바에서 동기화비동기화는 스레드가 자원에 접근할 때 동시에 접근할지 또는 순서대로 접근할지를 제어하는 개념이다. 

즉, 작업을 순서대로 차례 차례 진행할지 동시에 다발적으로 진행할지에 대한 것이다.

그림으로 비교해서 봐보자

 

먼저 비동기 작업의 예시이다.

비동기 작업시 두 개의 스레드가 동시에 작업을 진행한다.

동기 작업일 경우 Thread-0 작업이 끝난 후 Thread-1 작업이 진행된다. 

즉, 차례 차례 순서대로 스레드가 작업을 진행할 수 있다.

(실제로는 스레드 넘버와 관련 없이 랜덤으로 실행됨)

 

이번에는 위 예시 두개의 그림을 코드로 봐보자.

 

NonSyncTask는 비동기 작업, SyncTask는 동기 작업이며 작업을 실행하는 worker를 두개의 스레드에서 실행하도록한다.

public interface Task {
    void working();
}

// 비동기 작업
public class NonSyncTask implements Task {
    public void working(){
        System.out.println( "[" + Thread.currentThread().getName() + "]"+ " 비동기화 작업을 수행합니다.");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

// synchronized을 사용하여 동기 작업으로 만듦
public class SyncTask implements Task{
    public synchronized void working(){
        System.out.println( "[" + Thread.currentThread().getName() + "]"+ " 동기화인 작업을 수행합니다.");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class Worker implements Runnable{
    private final Task task;

    public Worker(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        task.working();
    }
}

 

위의 코드를 이용해 비동기 작업과 동기 작업의 처리 시간 차이를 볼 것이다.

똑같이 두개의 스레드로만 각각 작업을 실행시킬 것이므로 시간의 차이는 약 2배 정도 될 것이다.

 

- 비동기 작업 테스트 코드

public class NonSyncTest {
    public static void main(String[] args) throws InterruptedException {
        NonSyncTask nonSyncTask = new NonSyncTask();
        Worker worker = new Worker(nonSyncTask);
        Thread worker1 = new Thread(worker);
        Thread worker2 = new Thread(worker);

        long startTime = System.currentTimeMillis();
        worker1.start();
        worker2.start();

        worker1.join();
        worker2.join();
        long endTime = System.currentTimeMillis();

        System.out.println("비동기 작업 걸린 시간 : " + (endTime - startTime) + "ms");
    }
}

// 테스트 결과 
[Thread-1] 비동기화 작업을 수행합니다.
[Thread-0] 비동기화 작업을 수행합니다.
비동기 작업 걸린 시간 : 2017ms

 

 

- 동기 작업 테스트 코드

public class SyncTest {
    public static void main(String[] args) throws InterruptedException {
        SyncTask syncTask = new SyncTask();
        Worker worker = new Worker(syncTask);
        Thread worker1 = new Thread(worker);
        Thread worker2 = new Thread(worker);

        long startTime = System.currentTimeMillis();
        worker1.start();
        worker2.start();

        worker1.join();
        worker2.join();
        long endTime = System.currentTimeMillis();

        System.out.println("동기 작업 걸린 시간 : " + (endTime - startTime) + "ms");
    }
}

// 테스트 결과
[Thread-0] 동기화인 작업을 수행합니다.
[Thread-1] 동기화인 작업을 수행합니다.
동기 작업 걸린 시간 : 4020ms

 

결과는 예상했던대로다.

비동기 작업은 동시 작업했기 때문에 2초 정도 걸린 것이고

동기 작업은 하나의 작업이 끝나고 다음 작업을 진행했기 때문에 총 4초 정도 걸린 것이다.

 

비동기,동기는 여러 스레드가 작업을  동시에 진행할 수 있느냐, 없느냐의 차이이다.

그렇다면 블로킹과 논블로킹은 뭘까?

 

블로킹과 논블로킹

동기 작업의 예시를 다시 보자.

어떻게 순서대로 동기 작업이 진행 될 수 있었을까?

 

synchronized을 이해해야 한다.

synchronized 키워드를 사용하면 해당 객체에 모니터 락(monitor lock)이 존재하게 된다.

 

synchronized을 사용했기에 syncTask에 monitor lock 존재하게 되었다.

worker1이 syncTask의 lock을 먼저 획득하고 동기 작업인 working을 진행한다.

worker2 스레드는 대기 상태가 된다.

worker1의 작업이 끝나면 lock을 반납한다.

worker2가 syncTask의 lock 획득하고 동기 작업인 working을 진행한다.

 

worker2 스레드가 대기상태가 되는 것을 블로킹이라고 한다.

논블로킹으로 만드려면? 락을 획득을 위한 대기를 없애면 된다.

비동기 작업처럼 만들면 논블로킹이 되는 것이다.

 

위의 예시의 블로킹은 순서대로 진행하기 위해서 스레드가 대기를 해야하는 블로킹이였다. 

 

또한 I/O 작업시에도 작업이 완료되기를 기다리면서 블로킹이 될 수 도 있다.

I/O 작업(파일 읽기, 네트워크 통신, 데이터베이스 접근 등)은 상대적으로 속도가 느리기 때문에, 스레드가 I/O 작업이 완료되기를 기다리면서 블로킹(대기) 상태에 들어갈 수 있다.

예를 들어, 파일에서 데이터를 읽거나, 외부 API를 호출하거나, 데이터베이스에서 쿼리를 실행할 때 블로킹이 발생할 수  있다.

 

 

관점의 차이를 이해하자

서두에 아래와 같은 질문이 있었다.

"그렇다면 왜 작업을 진행하지 못하게 되는걸까?" 

 

동기화는 순서대로 작업을 처리하기 위해 락을 가진 스레드만 작업을 하고 다른 스레드는 작업을 진행하지 못한다.

블로킹은 작업 진행 중 처리 순서나, 다른 I/O 스레드로부터 데이터를 받기 위해 작업을 멈추고 기다리는 것이다. 

그렇기 때문에 작업을 진행하지 못한다.

 

작업을 진행하지 못하게 하는 이유는 관점의 차이이다.

 

동기,비동기는 작업을 동시에 처리할 것인가에 초첨을 맞춘 것이고

블로킹,논블로킹은 작업을 멈추게 할 것인가에 초첨을 맞춘 것이다.

서로 다른 초점에 맞춰져있다. 

즉, 관점이 다르다는 것이다.

 

정리

위 차이점이 결합되어 다음과 같은 조합이 될 수 있다.

  1. 동기 + 블로킹: 일반적인 순차적 작업 방식. 작업이 완료될 때까지 기다렸다가 다음 작업을 시작
  2. 동기 + 논블로킹: 작업을 요청한 후 즉시 반환되지만, 결과가 나올 때까지 다른 방식으로 지속적으로 작업 상태를 확인하는 방식
  3. 비동기 + 블로킹: 비동기 요청을 하고 다음 작업을 바로 시작하지만, 특정 요청이 완료되기 전까지는 결과를 기다리며 해당 스레드를 멈출 수 있음
  4. 비동기 + 논블로킹: 요청을 보내고 기다리지 않으며, 작업이 완료되면 콜백이나 이벤트로 결과를 처리

위에서 공부한 흐름대로 생각해보면 위의 조합을 이해하는데 어렵지 않을 것이다.