synchronize

한상우·2023년 5월 31일
0

java

목록 보기
12/16

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
안녕하세요 ^^

0개의 댓글