m1ndy5's coding blog

Ditto 프로젝트 동시성 문제 해결하기 4편 - Java에서 동시성을 제어하는 방법 (Synchronized, ConcurrentHashMap) 본문

Toy Projects/Ditto - Discuss Today's Topic

Ditto 프로젝트 동시성 문제 해결하기 4편 - Java에서 동시성을 제어하는 방법 (Synchronized, ConcurrentHashMap)

정민됴 2024. 3. 25. 01:06

파면 팔수록 굉장히 많은 것들이 나오는 동시성..!! 재밌구나 하하

이번에는 Java에서 동시성 이슈를 제어하는 방법에 대해 정리해보자.

 

1. Synchronized 키워드 : Synchronized 키워드를 사용하여 특정 메서드 또는 코드 블록을 한 번에 하나의 스레드만 실행하도록 한다.

public class SynchronizedExample {

    private int counter = 0;

    // synchronized 키워드를 사용하여 메서드를 동기화
    public synchronized void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        // 여러 스레드가 동시에 increment 메서드를 호출하여 counter를 증가시킴
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 예상 결과: 2000 (하지만 동시성 문제가 없으면 그렇지 않을 수도 있음)
        System.out.println("Counter: " + example.getCounter());
    }
}

위 예제에서는 increment() 메서드에 synchronized 키워드를 사용하여 여러 스레드가 동시에 이 메서드를 호출할 때 동기화를 보장한다.

 

단점 : synchronized 키워드는 실행 중인 스레드가 잠금을 획득하고 해제할 때까지 다른 스레드가 대기해야 함으로 성능 저하를 초래할 수 있다. 또한 단일 락을 사용하여 동시성을 제어하기 때문에, 많은 스레드가 동시에 실행될 때 성능이 저하될 수 있다.

 

 

2. 스레드 안전한 컬렉션 : Java는 동시성 컬렉션인 ConcurrentHashMap, ConcurrentLinkedQueue 등을 제공한다. 이런 컬렉션은 여러 스레드에서 안정하게 사용될 수 있다.

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        // ConcurrentHashMap 생성
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 여러 스레드가 동시에 맵에 접근하여 값을 추가
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                // putIfAbsent 메서드는 키가 없을 때만 값을 추가
                map.putIfAbsent("Key", 0);

                // 키 "Key"에 해당하는 값을 증가시킴
                map.compute("Key", (key, value) -> value + 1);
            }
        };

        // 여러 스레드 생성 및 실행
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();

        // 스레드 종료 대기
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 결과 출력
        System.out.println("Value for key 'Key': " + map.get("Key"));
    }
}

위 예시는 ConcurrentHashMap을 사용하여 여러 스레드가 동시에 맵에 접근하고 값을 증가시키는 상황이다.

putIfAbsent() 메서드를 사용하여 특정 키가 맵에 존재하지 않을 때만 값을 추가하고 compute() 메서드를 사용하여 해당 키의 값을 증가시켰다. 이러한 작업은 여러 스레드가 안전하게 동시에 실행될 수 있도록 동시성 기능을 이용하였다.

 

단점 : ConcurrentHashMap은 동시성 보장을 위해 내부적으로 많은 추가 메모리를 사용한다. 또한 맵을 반복하고 있는 동안 다른 스레드가 맵에 변경을 가할 경우, 반복 중인 스레드에게 이 변경 사항이 반영되지 않을 수 있다. 또한 서버가 종료될 때 ConcurrentHashMap에 있는 내용도 모두 손실되기 때문에 주기적으로 다른 저장 장치에 저장하는 작업도 필요하다.

 

결론

synchronized 키워드와 ConcurrentHashMap을 사용하여 동시성을 제어할 수 있다.

synchronized같은 경우 한 스레드가 잠금을 획들하고 해제할 때까지 다른 스레드는 대기해야하기 때문에 성능 저하가 발생한다.

ConcurrentHashMap 같은 경우 여러 스레드가 동시에 읽기 및 쓰기 작업을 수행할 수 있어 동시성을 보장하지만 반복 중에 일관성이 보장되지 않을 수 있고 추가 메모리를 많이 사용해야한다. 또한 서버가 종료될 때 저장된 내용이 날라가니 추가적으로 저장 장치에 저장하는 작업이 필요하다.