synchronize

한상우·2023년 5월 31일
0

java

목록 보기
12/16
post-custom-banner

synchronized

자바 메모리 영역


자바는 stack, heap, static 영역으로 이루어져 있음. 자바의 스레드끼리는 static 하고 heap 영역을 공유하기 때문에, 공유 자원에 대한 동기화 문제를 신경써야 한다.

동기화 문제를 해결하는 방법 중 하나인 Synchronized 키워드

sychronized 는 lock 을 통해 동기화를 시키며 4가지 사용법이 있다

synchronized method


public class SyncClass {

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

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

public class App {

    public static void main(String[] args) {
        SyncClass syncClass = new SyncClass();

        Thread thread1 = new Thread(() -> {
            System.out.println("thread1 start" + LocalDateTime.now());
            syncClass.syncMethod1("thread1");
            System.out.println("thread1 end" + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("thread2 start" + LocalDateTime.now());
            syncClass.syncMethod2("thread2");
            System.out.println("thread2 end" + LocalDateTime.now());
        });

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

SyncClass 인스턴스를 하나 생성하고 두 스레드를 만들어 각각 syncMethod1, syncMethod2 를 호출했다

// 결과
thread1 start2022-11-27T15:01:07.204569
thread2 start2022-11-27T15:01:07.204888
thread1의 syncMethod1 실행중 2022-11-27T15:01:07.230202
thread1 end2022-11-27T15:01:08.250856
thread2의 syncMethod2 실행중 2022-11-27T15:01:08.250880
thread2 end2022-11-27T15:01:09.256507

스레드1 이 syncMethod1 를 호출하고 종료된 다음 스레드2가 syncMethod2 를 호출하고 종료

인스턴스를 두개 만들고 실행시키면 결과값은 아래와 같다.

public class App {

    public static void main(String[] args) {
        SyncClass syncClass1 = new SyncClass();
        SyncClass syncClass2 = new SyncClass();

        Thread thread1 = new Thread(() -> {
            System.out.println("thread1 start" + LocalDateTime.now());
            syncClass1.syncMethod1("thread1");
            System.out.println("thread1 end" + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("thread2 start" + LocalDateTime.now());
            syncClass2.syncMethod2("thread2");
            System.out.println("thread2 end" + LocalDateTime.now());
        });

        thread1.start();
        thread2.start();
    }
}
thread2 start2022-11-27T15:10:46.957119
thread1 start2022-11-27T15:10:46.957458
thread1의 syncMethod1 실행중 2022-11-27T15:10:46.983359
thread2의 syncMethod2 실행중 2022-11-27T15:10:46.983279
thread1 end2022-11-27T15:10:48.005838
thread2 end2022-11-27T15:10:48.006050

lock 을 공유하지 않아, 스레드간 동기화가 발생하지 않는다.

synchronized method 는 인스턴스에 lock 을 건다. 인스턴스 접근 자체에 lock 을 거는게 아니라 synchronized 키워드가 붙은 메소드에 lock을 거는 것.

static synchronized method


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

  • static을 안붙힌 synchronized 메소드는 static을 붙인 synchronized 메소드와 동기화가 지켜지지 않는다.

싱글톤 객체에서의 동기화


public class Basic {

    private static Basic basic;

    public static Basic getInstance() {
        if (Objects.isNull(basic)) {
            basic = new Basic();
        }
        return basic;
    }
}

싱글 스레드 환경에서는 싱글톤 객체를 쉽게 위와 같은 방식으로 반환할 수 있지만, 멀티 스레드 환경에서는 getInstance 가 동시에 불릴 수 있어 동기화 문제가 발생한다.

getInstance에 synchronized 를 붙이면 동기화 이슈를 해결할 수 있지만, synchronized 메소드가 많으면 멀티 스레드는 병목현상이 발생할 수 있다. (기껏 멀티 스레드를 사용하는데, 싱글 스레드처럼 동작할 수 있게된다)

LazyHolder

라는 방법이 있는데… 굳이 알아야 하나?? 싶네..

기타


아래는 따른거 보면서 쪼금씩 추가하면서 지워


Thread 동기화

private static int count= 0;

    @Test
    void threadTest() throws InterruptedException {
        int maxCnt = 1000;

        for (int i=0; i<maxCnt; i++) {
            new Thread(() -> {
                count++;
                System.out.println(count);
            }).start();
        }

        Thread.sleep(1000);
        assertThat(count).isEqualTo(maxCnt);
    }

생성된 각 쓰레드들은 결국 전역변수 count를 참조하고 있는데,

count++ 연산이 원자성을 보장하지 않음 → 동시성 문제 발생할 수 있음

해결 방법

  1. synchronized
private static int count= 0;

    public synchronized void plus() { // 임계 영역임이 보장됨
        count++;
        System.out.println(); 
    }

    @Test
    void threadTest() throws InterruptedException {
        int maxCnt = 10000;

        for (int i=0; i<maxCnt; i++) {
            new Thread(this::plus).start();
        }
        Thread.sleep(100);
        assertThat(count).isEqualTo(maxCnt);
    }
  • 해당 스레드가 임계 영역을 빠져나가기 전까지 다른 스레드들은 동기화 처리된 블록에 접근할 수 없음
  • 하지만, 다른 스레드들은 아무런 작업을 하지 못하고 기다릴 수 밖에 없어.. 자원의 낭비 발생
  1. volatile
  • 각 쓰레드가 연산을 할 때 CPU에서는 성능 향상을 위해 메인 메모리에서 읽은 변수를 CPU캐시에 저장한다.
  • 멀티 쓰레드 환경에서 각 쓰레드가 어떤 값을 얻고 싶을 때 각 CPU 캐시에서 값을 얻어오기에 변수 값이 다를 수 있다.
  • 만약 여러 Thread가 write하는 상황이라면 적합하지 않다. 하나의 쓰레드만 write하고 나머지가 read할 때 적합하다 함
  1. Atomic (java.util.concurrent.*)
  • 비 원자적 연산 ( ++ 연산 같은게 비 원자적 연산)에서 동기화를 속도도 빠르게 이용하기 위한 클래스 모음.
  • 내부에 violatile 키워드 이용하면서 CAS(Compare and Swap) 알고리즘 사용하여 현재 스레드에 저장된 값과 메인 메모리에 저장된 값을 비교
  • 일치하면 thread safe한 상태이므로 로직 수행 일치 하지 않으면 재시도

정리

  • 동기화가 꼭 필요한 곳에만 synchronized 로 감싸주면 성능 하락이 많이 생기지 않는다. 하지만 실제 프로그램에서 동시성 문제가 예상되는 곳을 모두 synchronized를 통해 동기화를 하는건 코드가 너무 복잡하고 개발자가 고려해야 할게 많아진다.
  • 따라서 기본으로 제공하는 concurrent패키지의 클래스를 이용하는 것이 적절하다.

동시성 이슈

1) 가시성 문제

  • CPU 캐시 메모리와 RAM의 데이터가 서로 일치하지 않아 생기는 문제 → 변수를 캐시 메모리가 아닌 램에서 바로 읽도록 보장 (valatile) → 결국 공유 데이터를 읽는 경우의 동시성만 보장한다.

2) 원자성 문제

  • synchronized 또는 atomic을 통해 해결하면 가시성 문제도 해결
  • synchronized 블록 들어가기 전 CPU 캐시 메모리와 RAM을 동기화 해주고, atomic는 CAS 알고리즘에 의해 해결

(다시 보기)

스레드 풀

스레드가 요청이 올 때마다 생성하면 새로 생성하는 데 비용이 많이 든다.

왜?? 커널 레벨에서 다루기에 비용이 크게 발생한대

그래서 풀이라는 개념이 있는데

클라이언트가 작업을 요청하면 이 작업은

Task Queue에 해당 작업을 넣는다.

쓰레드 풀은 Task Queue에 존재하는 작업을 꺼내 쓰레드에게 실행하도록 함

장점:

비용적 측면, 컨텍스트 스위칭 딜레이 줄일 수 있음

단점:

너무 많은 양의 스레드를 만들면 메모리 낭비가 심해질 수 있음

profile
안녕하세요 ^^
post-custom-banner

0개의 댓글