Java

[Java] 고유락(Intrinsic Lock)과 동기화(synchronization)

DH_0518 2024. 3. 3. 12:33
동기화란?

 

멀티 스레드 환경에서는, 공유된 자원(변수나 객체 등)에 대해 여러 스레드가 동시에 접근하면서 예상치 못한 결과가 발생할 수 있다. 예시를 통해 알아보자

// Runnable을 구현해서 스레드 정의
public class RunnableThread implements Runnable{
    private final Number number;

    public RunnableThread(Number number) { this.number = number; }
	
    @Override
    public void run() {
        number.updateNum();
	    System.out.println("update End");
	}
}

// 공유된 자원
public class Number {
    private long num = 0L;
    
    public void updateNum(){
        for (long i = 0L; i < 1000000000L; i++ ) { //대략 10억번 연산
            this.num++;
        }
        System.out.println("num = " + num);
    }

    public long getNum() {
        return num;
    }
}

// 멀티 스레드 환경
public class TTest {
    public static void main(String[] args) {
        // 공유된 자원
        Number number = new Number();
        
        // 서로 다른 Thread 인스턴스 생성 -> 너무 많이 생성하면 컨텍스트 스위치 오버헤드가 발생함. 따라서 cpu 코어 개수 이하로 유지
        RunnableThread r1 = new RunnableThread(number);
        RunnableThread r2 = new RunnableThread(number);
        RunnableThread r3 = new RunnableThread(number);
        RunnableThread r4 = new RunnableThread(number);
        RunnableThread r5 = new RunnableThread(number);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        Thread t3 = new Thread(r3);
        Thread t4 = new Thread(r4);
        Thread t5 = new Thread(r5);

        // Thread 시작
        Instant startTime = Instant.now();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        // Thread 값 출력을 위한 종료 대기
        try {
            t1.join();
            t2.join();
            t3.join();
            t4.join();
            t5.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Thread 종료 후 num 값 출력
        System.out.println("expected num = " + 5_000_000_000L);
        System.out.println("final num = " + number.getNum());
        System.out.println("diff = " + (5_000_000_000L - number.getNum()));
        System.out.println("time = " + (Duration.between(startTime, Instant.now()).toMillis()) + "ms");
	}
}

테스트 결과

다음처럼 예상 결과는 50억이지만, 실제 결과는 대략 12억 4천만이 나왔다. 왜 이런 결과가 나왔을까?

바로 '++' 연산자가 thread-safe 하지 않기에, 멀티 스레드 환경에서 race condition이 발생하기 때문이다.

 

'++' 연산은 다음의 세 단계로 동작된다.

(1) 변수의 현재값을 읽어온다

(2) 읽어온 값에 1을 더한다

(3) 결과를 변수에 저장한다

 

만약 어떤 스레드에 의해서 (2)번 작업이 완료되고 (3)번이 일어나기 전에, 다른 스레드에 의해 다시 (1)번이 수행된다면, 결국 (2)번이 생략되고 (1) -> (3)이 일어난 결과가 발생한다. 따라서 1을 더하는 (2)번이 생략됐기에 예상 결과값보다 작은 결과값이 나오게 된다.

 

이런 식으로 여러 스레드가 동일한 자원에 동시에 접근하면 예상치 못한 결과가 발생할 수 있으므로, 동시에 접근하지 못하도록 제어하는 메커니즘을 동기화라고 한다.

 

 

 

 

 

Java의 동기화

 

 

Java에서는 멀티스레드 환경에서 동기화를 지원하기 위해 고유락(Intrinsic Lock)을 지원한다. 고유락은 Java의 모든 객체가 가지고 있는 lock으로, 여러 개의 스레드가 동일한 객체에 접근할 때 하나의 스레드만 접근을 허용하고 나머지는 순서대로 대기시키는 역할을 한다. 이는 프로세스 동기화 방법 중 모니터(Monitor) 방식과 같기에, 모니터라고도 부른다

 

 

Java의 동기화 방법, synchronized

  • Java에서는 synchronized 블록을 사용해서 동기화 처리를 할 수 있다
  • synchronized는 메서드에도 붙일 수 있다
  • 스레드는 synchronized 블록에 진입할 때 락을 획득하고, 블록을 벗어날 때 락이 해제된다
    이처럼 고유락을 이용하면 블록 단위로 락의 획득/해제가 일어나므로 구조적인 락(Structed lock)이라고 한다
  • static에 synchronized 키워드를 사용한 경우, static 메서드는 기반이 되는 객체가 없기에 Class 오브젝트의 고유락을 잡게 된다. 따라서 그 클래스 객체에 걸리는 고유락과 충돌하지 않는다
  • 생성자에는 synchronized를 사용할 수 없다. 단, 생성자의 body 부분에서 사용할 수 있다
  • synchronized 블럭의 내부에서 Thread.sleep()을 호출하더라도 고유락이 해제되지 않는다
  • synchronized 블럭에 들어가기 위해 경쟁하는 스레드에는 우선순위가 없다. 그냥 먼저 잡는 스레드가 우선 실행된다
  • 또한 Java의 고유락은 재진입(Reentrancy)이 가능하다. 따라서 락을 획득한 스레드는, 새로운 snchronized를 만나더라도 대기 없이 통과가 가능하다
    -> 이는 락의 획득이 호출 단위가 아닌 스레드 단위로 일어난다는 것을 의미한다
// Runnable을 구현해서 스레드 정의
public class RunnableThread implements Runnable{
    /**
     * 동일 코드
     */
}

// 공유된 자원
public class Number {
    private int num = 0;
    
    /**
     * synchronized 블럭을 통해 Thread-safe를 보장한다
     */
    public synchronized void updateNum(){
        for (long i = 0L; i < 1000000000L; i++ ) { //대략 10억번 연산
            this.num++;
        }
        System.out.println("num = " + num);
    	// synchronized(this) { for() ~ this.num++; } 와 동일한 코드임
    }

    public int getNum() {
        return num;
    }
}

// 멀티 스레드 환경
public class TTest {
    public static void main(String[] args) {
        /**
         * 동일 코드
         */
         
        // Thread 종료 후 num 값 출력
        System.out.println("expected num = " + 5_000_000_000);
        System.out.println("final num = " + number.getNum());
        System.out.println("diff = " + (5_000_000_000 - number.getNum()));
        System.out.println("time = " + (Duration.between(startTime, Instant.now()).toMillis()) + "ms");
	}
}

테스트 결과

위의 경우에는 synchronized 키워드가 this 객체의 고유락을 사용한 경우이다.

 

동일한 코드지만 updateNum 메서드에서 synchronized 블럭을 사용한 결과 diff = 0인 것을 확인할 수 있다.

이처럼 멀티 스레드 환경에서 연산이 정확해야하는 공유된 자원을 사용하는 경우, 동기화를 사용하여 Thread-safe 하게 관리할 수 있다.

 

 

 

 

 

 

Synchronized vs ASynchronized

 

 

하지만 비동기가 단점만 존재하는 것은 아니다. 여러 스레드가 순서에 상관없이 동작하므로, 대기하는 스레드가 없어서 오버헤드가 발생하지 않는다. (단, 스레드 수가 cpu 코어 수보다 많아진다면 컨텍스트 스위칭 때문에 비동기에서 속도가 더 느려질 수도 있다)

다음은 실행시간을 비교한 결과이다

  1차(ms) 2차(ms) 3차(ms) 4차(ms) 5차(ms) Average(ms)
ASynchronized 1233 1281 1261 1262 1250 1257.4
Synchronized 1848 1881 1854 1845 1895 1864.6
Average Diff 607.2

 

이처럼, 정확하기보다는 러프한 값이더라도 빠른 연산이 필요하다면 비동기를 사용하는 것이 더욱 효과적일 수 있다. 따라서 각각 상황에 알맞는 방법을 선택하는 것이 중요하다

 

 

 

 

 

 

 

 

 

 

복습 Question

  • Asynchronized에서 Thread가 일정 수준 이상으로 많아지면 synchronized보다 성능이 저하되는데 그 이유가 무엇일까?
  • synchronized 블럭에 들어가기 위해 경쟁하는 스레드의 우선순위는 어떻게 될까?
  • 락의 획득은 호출단위 vs 스레드단위 둘 중 어느 것일까?

 

 

 

 

 

 

 

Reference

 

<CS 지식> Java 고유락 (Intrinsic Lock)

자바의 모든 객체는 lock을 가지고 있음모든 객체가 가지고 있기 때문에 고유 락 (Intrinsic lock)이라고 하고, 모니터처럼 동작한다고 하여 monitor lock 혹은 monitor 라고 함위와 같은 코드가 있을 때, 1.

velog.io

 

[Java] 고유 락 (Intrinsic Lock) | 👨🏻‍💻 Tech Interview

[Java] 고유 락 (Intrinsic Lock) Intrinsic Lock / Synchronized Block / Reentrancy Intrinsic Lock (= monitor lock = monitor) : Java의 모든 객체는 lock을 갖고 있음. Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제

gyoogle.dev

 

[Java] synchronized 키워드 -고유락(Intrinsic Lock)

자바는 멀티스레드 환경에서 동기화를 지원하기 위해 가장 기초적인 장치인 '고유락(Intrinsic Lock)'을 지원한다.(고유락은 모니터락(Monitor lock) 혹은 모니터(monitor)라고 부르기도 한다) 개발자는 syn

hbase.tistory.com

 

[운영체제(OS)] 6. 프로세스 동기화(Process Synchronization)

[목차] 1. Race Condition 2. Critical Section 3. Synchronization Algorithms 4. Synchronization Hardware 5. Mutex Locks 6. Semaphores 7. Classical Problems of Synchronization 8. Monitor 참고) - https://parksb.github.io/article/10.html - KOCW 공개강의

rebro.kr