Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

To Dare Is To Do!

synchronized 본문

Java

synchronized

Nick_Choi 2024. 7. 10. 12:24

자바의 메모리 구조는 크게 method(static) 영역, stack 영역, heap 영역으로 구성되어 있다.

https://whitehartlane.tistory.com/24

 

자바 메모리 구조 (feat 자바 변수)

이전에 자바의 컴파일 과정에 대해 정리해본 글을 살펴보면JVM의 클래스 로더는 자바 컴파일러가 컴파일한 클래스 파일(바이트 코드)를 동적 로딩의 과정을 통해여 JVM의 메모리(Runtime Data Area)에

whitehartlane.tistory.com

이 중 static영역과 heap 영역은 자바의 멀티 스레드 환경에서 스레드들끼리 공유되기 때문에 공유자원에 대한 동기화 문제를 고려해야 한다.

 

스레드 동기화

위에서 말했 듯 멀티스레드 환경에서는 다른 스레드에 의해 공유자원의 값이 쉽게 변경될 수 있는 문제가 있다.

출처 이것이 자바다

위의 사진을 보면 User1Thread가 Calculator 객체라는 공유자원*memory 필드(임계영역*)에 100을 저장하고 2초간 일시 정지 상태가 된다.

그동안 User2Thread가 memory 필드값을 50으로 변경할 때, 2초가 지나 User1Thread가 다시 실행 상태가 되어 출력을 한다면 본래 기대했던 100이 아닌 50이라는 값을 출력하게 된다. 이렇게 되면 User1Thead는 기존의 데이터를 날리게 된다.

이렇게 스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드의 작업이 끝날 때까지 다른 스레드가 이용하지 못하도록 객체를 잠그면 된다. 

 

이를 위해 자바에서는 Synchronized 키워드를 사용하여 메서드 또는 코드 블록을 임계 영역으로 지정할 수 있다.

- Synchronized로 지정된 영역은 스레드간 공유 자원에 대한 접근을 동기화 한다.

- 스레드가 해당 영역에 접근하려면 모니터 락(고유 락=모니터)을 획득해야 한다.

- 만약 다른 스레드가 모니터 락을 보유하고 있다면 해당 스레드는 락이 해제될 때까지 기다려야 한다.

 

공유자원 : 여러 스레드가 동시에 접근할 수 있는 자원

임계영역 : 공유자원들 중 여러 스레드가 동시에 접근했을 때 문제가 생길 수 있는 부분

 

메서드 단위 락

- 해당 메서드 전체가 임계역역으로 설정됨

- 해당 메서드를 호출하는 객체의 인스턴스나 클래스에 대한 동기화를 제공

public synchronized void method() {
// 단 하나의 스레드만 실행하는 영역
}

 

코드 블럭 락

- 공유 자원에 대한 접근이 필요한 부분만 동기화 -> 다른 작업들과 병행 실행이 가능

- 메서드 단위 락에 비해 임계 영역을 줄여 스레드의 안정성과 동시성을 향상시킴

public void method(){
// 여러 스레드가 실행할 수 있는 영역

    synchronized (공유객체) {
    // 단 하나의 스레드만 실행하는 영역
    }

// 여러 스레드가 실행할 수 있는 영역

}

 

 

synchronized 블록 범위를 너무 작게 줄여 단일 연산으로 처리해야하는 작업을 분리하지 않도록 주의해야 한다.

=> ex) 어떤 연산이 여러 단계로 이루어져 있고 이 모든 단계를 동기화해야 일관성이 유지될 경우, 이 단계를 나눠서 각각의 단계마다 별도로 synchronized를 사용하면 스레드 간의 경쟁 조건(race condition)이 발생, 이는 데이터의 무결성(integrity)을 해칠 수 있음

 

단일 연산 구조에 문제가 생기지 않더라도 너무 잘게 쪼개는 것은 피하는 것이 좋다.

=> 동기화가 필요한 블록을 너무 잘게 쪼개면, 실제 연산보다 락을 얻고 놓는 오버헤드가 더 커질 수 있고 이는 성능 저하로 이어질 수 있음

 

 

synchronized method

public class Method {

    public static void main(String[] args) {

        Method method = new Method();
        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            method.syncMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            method.syncMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("스레드3 시작 " + LocalDateTime.now());
            method.method3("스레드3");
            System.out.println("스레드3 종료 " + LocalDateTime.now());
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

    private synchronized void syncMethod1(String msg) {
        System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void syncMethod2(String msg) {
        System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void method3(String msg) {
        System.out.println(msg + "의 method3 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 실행 결과
스레드2 시작 2021-12-20T17:49:44.512644400
스레드3 시작 2021-12-20T17:49:44.512644400
스레드1 시작 2021-12-20T17:49:44.512644400
스레드3의 method3 실행중2021-12-20T17:49:44.529689700
스레드2의 syncMethod2 실행중2021-12-20T17:49:44.529689700
스레드2 종료 2021-12-20T17:49:49.539983200
스레드3 종료 2021-12-20T17:49:49.539983200
스레드1의 syncMethod1 실행중2021-12-20T17:49:49.539983200
스레드1 종료 2021-12-20T17:49:54.540973700

Method의 인스턴스 하나만 생성 후 두 개의 스레드를 만든 상황에서 synchronized 키워드가 붙은 메서드 2개와 붙지 않은 메서드 하나를 호출했다.

실행 결과를 보면 스레드들이 동일한 우선순위를 갖기 때문에 무작위로 시작되었으나 synchronized 키워드가 붙은 스레드2가 종료된 후에 스레드1가 실행된 것을 확인할 수 있다. 

하지만 synchronized가 붙지 않은 메서드는 동기화가 발생하지 않아 다른 스레드와 함께 실행되고 종료된 것을 확인할 수 있다.

이를 통해 synchronized method가 인스턴스 단위로 lock을 걸고 synchronized 키워드가 붙은 메서드끼리만 lock을 공유한다는 것을 확인할 수 있다.

 

static synchronized method

public class StaticMethod {

    public static void main(String[] args) {
        StaticMethod staticMethod = new StaticMethod();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            syncStaticMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            syncStaticMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("스레드3 시작 " + LocalDateTime.now());
            staticMethod.syncMethod3("스레드3");
            System.out.println("스레드3 종료 " + LocalDateTime.now());
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

    public static synchronized void syncStaticMethod1(String msg) {
        System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void syncStaticMethod2(String msg) {
        System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void syncMethod3(String msg) {
        System.out.println(msg + "의 syncMethod3 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 실행 결과
스레드2 시작 2021-12-20T18:16:47.589799300
스레드1 시작 2021-12-20T18:16:47.589799300
스레드3 시작 2021-12-20T18:16:47.589799300
스레드2의 syncStaticMethod2 실행중2021-12-20T18:16:47.605801200
스레드3의 syncMethod3 실행중2021-12-20T18:16:47.605801200
스레드2 종료 2021-12-20T18:16:52.615917600
스레드3 종료 2021-12-20T18:16:52.615917600
스레드1의 syncStaticMethod1 실행중2021-12-20T18:16:52.615917600
스레드1 종료 2021-12-20T18:16:57.616291400

static 키워드가 포함된 synchronized 메소드는 인스턴스가 아닌 클래스 단위로 lock 을 공유한다.

static sychronized 메소드를 사용하는 스레드1과 스레드2 간에는 동기화가 잘 지켜지는 것을 확인할 수 있지만 sychronized 메소드를 사용한 스레드3은 의도대로 동기화가 지켜지지 않았다.

=> 클래스 단위에 거는 lock 과 인스턴스 단위에 거는 lock 은 공유가 안 되기 때문에 혼용해서 쓰게 된다면 동기화 이슈가 발생

 

synchronized block

synchronized block은 인스턴스의 block 단위로 lock 을 걸며, 2가지의 사용 방법이 있다.

synchornized(this)

public class Block1 {

    public static void main(String[] args) {

        Block1 block = new Block1();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            block.syncBlockMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            block.syncBlockMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });
        thread1.start();
        thread2.start();
    }

    private void syncBlockMethod1(String msg) {
        synchronized (this) {
            System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void syncBlockMethod2(String msg) {
        synchronized (this) {
            System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 실행 결과
스레드2 시작 2021-12-20T18:40:11.652664400
스레드1 시작 2021-12-20T18:40:11.652664400
스레드2의 syncBlockMethod2 실행중2021-12-20T18:40:11.668626700
스레드2 종료 2021-12-20T18:40:16.675778600
스레드1의 syncBlockMethod1 실행중2021-12-20T18:40:16.675778600
스레드1 종료 2021-12-20T18:40:21.676119600

synchronized 인자값으로 this 를 사용하면 모든 synchronized block 에 lock 이 걸린다. 

여러 스레드가 들어와서 서로 다른 synchronized block 을 호출해도 this를 사용해 자기 자신에 lock 을 걸었기 때문에 기다려야 한다.


synchornized(Object)

public class Block2 {

    private final Object o1 = new Object();
    private final Object o2 = new Object();

    public static void main(String[] args) {

        Block2 block = new Block2();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            block.syncBlockMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            block.syncBlockMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });
        thread1.start();
        thread2.start();
    }

    private void syncBlockMethod1(String msg) {
        synchronized (o1) {
            System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void syncBlockMethod2(String msg) {
        synchronized (o2) {
            System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 실행 결과
스레드2 시작 2021-12-20T18:44:09.476825500
스레드1 시작 2021-12-20T18:44:09.476825500
스레드2의 syncBlockMethod2 실행중2021-12-20T18:44:09.494827100
스레드1의 syncBlockMethod1 실행중2021-12-20T18:44:09.494827100
스레드2 종료 2021-12-20T18:44:14.504974100
스레드1 종료 2021-12-20T18:44:14.504974100

스레드들끼리 동기화가 이뤄지지 않았다.

o1 과 o2처럼 객체를 따로 만들어 인자로 넘겨주면 동시에 lock 이 걸려야 하는 부분을 따로 지정해 줄 수 있다.

 

static synchronized block

static method 안에 synchronized block을 지정할 수 있다. 

static의 특성상 this같이 현재 객체를 가르키는 표현을 사용할 수 없다.

static synchroinzed method방식과 차이는 lock객체를 지정하고 block으로 범위를 한정지을 수 있다는 점이다. 

이외에 클래스 단위로 lock을 공유한다는 점은 같다.

 

wait(), notify()

- synchronized 메서드나 블럭에서 호출할 수 있는 메서드

wait()

해당 메서드를 호출하는 스레드는 해당 객체의 모니터 락을 해제하고 notify()나 notifyAll()이 호출될 때까지 wait 상태로 전환됨

notify()

해당 메서드를 호출하면 해당 객체의 대기열에서 하나의 스레드를 임의로 선택하여 깨움

notifyAll()

해당 메서드를 호출하면 대기중인 모든 메서드를 깨워 해당 객체의 모니터 락을 얻기 위한 경쟁을 일으킴

락을 얻으면 작업을 수행하나 얻지 못하면 blocked 상태 유지

 

특징

재진입 가능성(reentrancy)
synchronized 영역 내에서 자신이 이미 보유한 락을 다시 획득할 수 있는 기능인 재진입을 지원한다.

=> 자신이 가진 잠금을 다시 잠그려 할 때 허용한다.
이로 인해 같은 스레드 내에서 재귀적인 호출이나 상속 관계에서도 안전하게 동작함

구조적인 락(structed lock)
락이 스택처럼 쌓인다. A획득 → B획득 → B해제 → A해제 이런 식으로 동작한다.

가시성(visibility)
스레드들이 순서대로 실행되기 때문에, 이전의 스레드가 쓴 값을 읽을 수 있다.  

데드락의 위험성
synchronized 키워드를 무분별하게 사용하다간, 데드락이 발생하기 쉽다.
따라서 synchronized block 안에 다른 synchronized block을 둘 때는 신중해야 한다.