synchronized(동기화)

sungs·2025년 7월 11일

자바

목록 보기
38/95

synchronized

이름 그대로 동기화다. 공유 자원에 동시에 접근하게 되는 경쟁 상태를 해결하고 안전한 임계 영역을 만들기 위해 쓰여진다.
좀 더 간단히 말하자면 멀티 스레드 상황에서 차례차례 스레드가 한 개씩 작업하게 되어 원하는 결과값이 나오게 된다. (동시에 작업할 경우 중간에 값 변경이 겹치면서 값이 제대로 안 나올 수 있음.)
원리는 간단하다. 이를 메서드나 블록에 걸면 단 한 가지의 스레드만이 접근할 수 있게 되는 것이다.

public class Counter {
    private int count = 0;

    // synchronized 메서드: 이 메서드는 한 번에 하나의 스레드만 접근 가능
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " - Increment: " + count);
    }

    public synchronized void decrement() {
        count--;
        System.out.println(Thread.currentThread().getName() + " - Decrement: " + count);
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // 스레드 10개 생성하여 increment 메서드 호출
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            }, "Thread-" + i).start();
        }

        // 스레드 10개 생성하여 decrement 메서드 호출
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.decrement();
                }
            }, "Thread-" + i).start();
        }

        // 모든 스레드가 종료될 때까지 기다리는 로직은 생략.
        // 실제로는 CountDownLatch 등을 사용하여 모든 스레드 종료를 기다릴 수 있습니다.
        try {
            Thread.sleep(2000); // 충분히 기다려서 스레드들이 작업을 마칠 시간을 줍니다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final count: " + counter.getCount());
    }
}
public class SharedResource {
    private int data = 0;
    private final Object lock = new Object(); // 락으로 사용할 객체

    public void modifyData() {
        System.out.println(Thread.currentThread().getName() + " - Trying to acquire lock...");
        // this 대신 특정 객체(lock)를 사용하여 락을 건다.
        synchronized (lock) { // lock 객체에 락을 설정
            System.out.println(Thread.currentThread().getName() + " - Lock acquired. Modifying data...");
            // 이 블록 내부의 코드는 한 번에 하나의 스레드만 실행 가능
            data++;
            try {
                Thread.sleep(100); // 작업 수행을 시뮬레이션
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + " - Data updated to: " + data);
        } // 락 해제
        System.out.println(Thread.currentThread().getName() + " - Lock released. Continue other tasks...");
    }

    public int getData() {
        return data;
    }

    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        // 5개의 스레드가 modifyData 메서드 호출
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                resource.modifyData();
            }, "Worker-" + i).start();
        }

        try {
            Thread.sleep(1000); // 충분히 기다려서 스레드들이 작업을 마칠 시간을 줍니다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final data: " + resource.getData());
    }
}

이처럼 사용 방법도 어렵지 않다.

이렇게 되면 같은 인스턴스에 접근하더라도 한 번에 한 스레드만 접ㄷ근하게 되어 그 스레드가 작업하던 중 필드값이 예상치 못한 값을 바뀌지 않게 된다.
즉, 동시성 문제를 해결하게 되는 것이다.
참고로 이럴 때는 volatile도 사용 안 해도 된다.

작동 원리에 대해 설명하지면, 인스턴스마다 고유의 모니터 락을 가지고 있는데 이 락으로 동기화되어있는 메서드나 블록에 접근할 수 있다. 그래서 가장 먼저 접근한 스레드가 인스턴스로부터 이 락을 얻게 된다. 따라서 가장 먼저 접근한 스레드 한 개만 작업을 하고 나머지 스레드는 락이 없어 blocked 상태에 빠지게 된다.
첫 번째 스레드의 작업이 완료되면 락은 반환되고 다른 스레드가 그 락을 얻어 작업을 이어서 하게 된다. 참고로 락을 얻는 우선 순위는 정해져 있지 않다. 뒤에 있는 스레드가 얻을 수도 있다.

단점

다만, volateile처럼 남용하게 되면 성능 저하가 생길 수 있다.
10차선 도로를 1차선 도로와 만드는 것과 같이 10개의 스레드가 가능한 작업을 굳이 1개의 스레드만 작업하게 하면 당연히 성능 저하가 생길 수밖에 없다.
그리고 어떤 스레드는 락을 못 얻어 무한정 대기하게 될 수도 있다. 게다가 락의 우선 순위도 없어 누가 먼저 락을 얻을 지 알 수 없다.
따라서 좀 더 유연하게 사용하기 위해 cocurrent라는 게 등장하게 된다.

profile
앱 개발 공부 중

0개의 댓글