일단 진행시켜

Java 동시성: 가시성 및 동기화 본문

🗞️ 보안 동향 파악 및 나의 생각 정리

Java 동시성: 가시성 및 동기화

2024. 8. 29. 14:48

1. Java Concurrency: Visibility and Synchronized

https://dzone.com/articles/java-concurrency-visibility-and-synchronized

 

 

[ 요약 ]

두 스레드 간의 변수 가시성과, 공유 변수를 변경할 때 발생하는 일에 대해 알아보자

 

지양해야 할 코드

import java.util.Date;
 
public class UnSynchronizedCountDown {
    private int number = Integer.MAX_VALUE;
 
    public Thread countDownUntilAsync(final int threshold) {
        return new Thread(() -> {
            while (number>threshold) {
                number--;
                System.out.println("Decreased "+number +" at "+ new Date());
            }
        });
    }
 
    private void waitUntilThresholdReached(int threshold) {
        while (number>threshold) {
        }
    }
 
    public static void main(String[] args) {
        int threshold = 2125840327;
 
        UnSynchronizedCountDown unSynchronizedCountDown = new UnSynchronizedCountDown();
        unSynchronizedCountDown.countDownUntilAsync(threshold).start();
 
        unSynchronizedCountDown.waitUntilThresholdReached(threshold);
        System.out.println("Threshold reached at "+new Date());
    }
}

 

주어진 코드는 멀티스레드 프로그래밍에서 메모리 가시성과 동기화와 관련된 문제가 있는 코드다. 

주어진 코드는 두 개의 스레드가 'number'라는 동일한 변수를 사용하여 작업한다.

countDownUntilAsync()는 'number' 를 감소시키는 작업을 수행한다. 

main()는 'number' 가 특정 임계값 이하로 떨어질 때까지 기다린다.

크게  두 가지 문제점이 존재한다.

 

문제점 1: 동기화 부족

Java에서 여러 스레드가 같은 변수를 동시에 접근하고 변경할 때, 이 변수를 동기화하지 않으면 예기치 않은 결과가 발생할 수 있다.
  • 변수 number가 두 개의 스레드에서 동시 접근을 한다
  • 첫 번째 스레드는 변숫값 감소, 두 번째 스레드는 변수를 읽고 있다
  • 여기서, 두 스레드가 동시에 number에 접근할 때 이를 적절히 동기화하지 않아, 두 스레드가 서로의 작업 결과를 올바르게 인식하지 못할 가능성이 있다

 

문제점 2: 메모리 가시성 문제

각 스레드가 자신만의 캐시를 가지고 작업을 수행하게 된다면, 한 스레드가 특정 변수의 값을 변경하더라도 다른 스레드에서 그 변경 사항을 바로 인식하지 못할 수 있다.
  • 첫 번째 스레드는 number를 감소시키지만, 메인에서는 변경된 값을 읽을 수 없다.
  • 따라서, 메인 스레드는 number 값이 임계값 이하로 떨어져도 이를 인식하지 못해 무한 루프에 빠질 수 있

 

 

 

해결➡️ 동기화(synchronized) 사용

  • synchronized 블록을 사용하면 여러 스레드가 동시에 변수 number에 접근하는 것을 방지할 수 있다
  • synchroinzed 블록에 걸려 넘어진 다른 스레드에서 스레드의 변경 사항을 볼 수 있다
package com.gkatzioura.concurrency.visibility;
 
 
public class SynchronizedCountDown {
    private int number = Integer.MAX_VALUE;
    private String message = "Nothing changed";
    private static final Object lock = new Object();
 
    private int getNumber() {
        synchronized (lock) {
            return number;
        }
    }
 
    public Thread countDownUntilAsync(final int threshold) {
        return new Thread(() -> {
            message = "Count down until "+threshold;
            while (number>threshold) {
                synchronized (lock) {
                    number--;
                    if(number<=threshold) {
                    }
                }
            }
        });
    }
 
    private void waitUntilThresholdReached(int threshold) {
        while (getNumber()>threshold) {
        }
    }
 
    public static void main(String[] args) {
        int threshold = 2147270516;
 
        SynchronizedCountDown synchronizedCountDown = new SynchronizedCountDown();
        synchronizedCountDown.countDownUntilAsync(threshold).start();
        System.out.println(synchronizedCountDown.message);
 
        synchronizedCountDown.waitUntilThresholdReached(threshold);
        System.out.println(synchronizedCountDown.message);
    }
}

 

getNumber()에서 동기화 블록을 통해 number 값을 반환한다.

이로 인해 해당 메서드에 접근하는 스레드는 lock 객체를 잠궈, 다른 스레드가 해당 블록에 들어오는 것을 막는다.

 

countDownUntilAsync()에서 number 감소시키는 로직을 블록 안에 넣어, lock 객체를 사용하여 잠근다.

number 변수 값 변경이 다른 스레드에도 잘 반영되도록 한다.

 

 

위 수정된 코드를 통해 가시성이 보장된다.

 

 

 

Java 가시성 보장?

여러 스레드가 동일한 변수에 접근할 때, 한 스레드가 해당 변수를 변경한 것이 다른 스레드에서도 즉시 확인될 수 있도록 하는 것
➡️ 스레드 간에 일관된 데이터 공유를 보장
  • synchronized 블록이 사용되면, 해당 블록 내에서 변경된 모든 변수의 상태는 블록이 끝날 때 메인 메모리에 반영됨
  • 이로 인해, 다른 스레드가 동일한 lock을 사용하여 동기화된 블록에 접근할 때 변수의 최신 값을 볼 수 있게 된다

 

 

 


 

🤔 이에 대한 나의 생각

디버깅으로 보이지 않는 오류라, 가시성 문제의 중요성이 높다고 생각한다.

나 또한 가시성의 중요성을 크게 생각하지 않았었는데, 예상치 못한 동작이 발생하고 특히 디버깅이 어렵다는 점에서 중요함을 깨달았다.

synchronized를 사용하면 가시성 문제를 해결할 수 있지만, 성능 비용에 문제가 있다. 스레드의 자원 대기 시간은 곧 애플리케이션의 성능과도 연결된다.

따라서 무작정 동기화를 챙기기보다 "성능이 중요한 애플리케이션에서는 적절히 사용하는 것" 이 중요해 보인다.