Java

자바의 고오급 동기화 concurrent.Lock

데일리코딩 2024. 9. 14. 15:55

안녕하세요 요즘에는 동기화를 학습중이라 동기화 관련 포스트만 하게 되네요 ㅎㅎ

아무래도 요즘 신입 개발자들에게 친숙하지 않는 개념이라고 생각이라고 드는데

 

사실 동기화 문제는 이해하기가 굉장히 어렵습니다. 공부하기도 쉽지가 않고 

학습을 한다 하더라도 어떻게 순서대로 학습해야하는지 시간을 많이 사용해야 하는데

그런분들을 위해서 조금이나마 도움이 됬으면 하는 마음으로 이번 포스팅 올리도록 하겠습니다.


지난 포스팅 synchronized 의 복습 

지난 포스팅에 내용에 우리는 자바가 무려 1.0부터 지원해준 synchronized 키워드 덕분에 동시성 이슈를 간단하게 설명할 수 있다고

그때 설명드렸습니다. 

 

사용방법은 간단하게 각 함수 앞에 synchronized 키워드만 붙여주면 동기화 문제를 해결할 수 있다고 말씀드렸구요

 synchronized 출금() {
    1. 검증 단계: 잔액(balance) 확인
    2. 출금 단계: 잔액(balance) 감소
}

그리고 synchronized 내부 동작방식에도 설명을 드렸죠?

자바의 synchronized 포스팅을 못보신 분이라면 먼저 방문해주세요!

https://junghan49.tistory.com/27

 

살짝 복습을 요약하자면...

1. 모든 객체에는 모니터 락을 가지고 있다.

2. synchronized 키워드를 붙이면 해당 함수는 임계영역이 설정이 된다.

3. 임계영역에는 각 스레드가 모니터 락을 획득해야지만 해당 임계영역에 들어와서 작업을 할 수 있다.

4. 작업을 완료한 스레드는 락을 반납하고 뒤에 기다리는 스레드가 다시 획득할 수 있다.

5. 동시성 이슈를 해결했지만 병목현상으로 일어나므로 함수에다가 키워드를 붙이기 보다는 임계영역을 설정해서 사용하는걸 권장한다.

 

이렇게 정리 할 수 있겠습니다!


synchronized 한계 그리고 자바의 고급 동기화 concurrent.Lock

synchronized 는 자바 1.0 부터 제공하는 아주 편리한 기능이지만 몇가지 한계가 존재합니다.

 

1. 무한대기: Blocked 상태의 스레드는 락이 풀릴 때 까지 무한 대기합니다..

2. 공정성: 락이 돌아오고 나서 Blocked 상태의 여러 스레드중 어떤 스레드가 락을 획득할지 모릅니다. 최악의 상황에는 특정 스레드만 계속 락을 획득 못할 수도 있음

 

자바에서는 이런 문제를 해결하기 위해서 1.5 버전부터 java.util.concurrent 라이브러리 패키지를 추가했습니다.

여기서 많은 인터페이스, 클래스가 존재하지만 오늘 집중할 인터페이스는 LockSupport 인터페이스 입니다.


LockSupport 기능

LockSupport는 스레드를 waintg 상태로 변경합니다.

wating 상태는 누가 깨워주기 전까지는 계속 대기하는 상태입니다. 그리고 cpu 실행 스케줄링에 들어가지 않습니다.

 

LockSupport 대표적인 기능을 소개드리겠습니다.

park() : 스레드를 `WAITING` 상태로 변경한다.
스레드를 대기 상태로 둔다. 참고로 `park` 의 뜻이 "주차하다", "두다"라는 뜻이다.

parkNanos(nanos) : 스레드를 나노초 동안만 `TIMED_WAITING` 상태로 변경한다.
지정한 나노초가 지나면 `TIMED_WAITING` 상태에서 빠져나오고 `RUNNABLE` 상태로 변경된다.

unpark(thread) : `WAITING` 상태의 대상 스레드를 `RUNNABLE` 상태로 변경한다.
더 많은 정보를 확인하고 싶다면 아래 링크를 확인하세요
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/LockSupport.html

 

 

package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class LockSupportMainV1 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ParkTask(), "Thread-1");
        thread1.start();

        // 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다.
        sleep(100);
        log("Thread-1 state: " + thread1.getState());
        log("main -> unpark(Thread-1)");

        LockSupport.unpark(thread1); // 1. unpark 사용
        // thread1.interrupt(); // 2. interrupt() 사용
    }

    static class ParkTask implements Runnable {
        @Override
        public void run() {
            log("park 시작");
            LockSupport.park();
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}

 

실행결과
09:58:16.802 [Thread-1] park 시작

09:58:16.889 [main] Thread-1 state: WAITING

09:58:16.889 [main] main -> unpark(Thread-1)

09:58:16.889 [Thread-1] park 종료, state: RUNNABLE

09:58:16.891 [Thread-1] 인터럽트 상태: false

실행상태의 그림

1. 메인스레드가 thread-1을 start() 하면 thread-1은 Runnable 상태로 변경됩니다.

2. Thread-1은 Thread.park() 를 호출합니다. Thread-1 은 Runnable -> Waintg 상태가 되면서 대기합니다.

3. main 스레드가 Thread-1을 unpark() 를 호출해서 깨웁니다. Thread-1은 대기상태에서 실행 가능 상태로 변경합니다.

이처럼 LockSupport는 특정 스레드를 Wating 혹은 Runnable 상태로 변경할 수 있습니다.

 

이는 기존 자바의 1.0 버전에 있던 synchronized 키워드 보다 좀더 세밀하고 정교하게 컨트롤 할 수 있다는 장점이 있습니다.

* 참고로 대기상태로 들어가는 Thread인 LockSupport.park() 는 매개변수 없이 스스로 대기상태에 들어갈 수 있고 
unpark(Thread thread)의 경우 특정 스레드를 지정하는 매개변수가 있는 이유는
스레드는 스스로 대기상테에 들어갈 수 있지만, 반면 깨워나는 건 스레드 자신만의 코드로 실행 할 수 없기 때문(대기 상태이니깐!)입니다.

 


LockSupport 정리

LockSupport를 사용하면 스레드를 wating, timed_wating 상태로 변경할 수 있고, 또 인터럽트를 받아서 스레드를 깨울 수도 있습니다.

이런 기능들을 잘 활용하면  synchronized의 단점인 무한대기 문제를 해결할 수 있습니다.

 

다음 포스팅은 LockSupport 인터페이스를 구현한 ReentantLock 에 대해서 학습을 진행하도록 하겠습니다.

긴글 읽어주셔서 감사합니다!

'Java' 카테고리의 다른 글

[Java] 스레드 풀과 Excutor 프레임워크  (2) 2024.09.29
[Java]ReentrantLock  (9) 2024.09.18
Java 프로세스와 스레드  (4) 2024.08.30