인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

도토리님의 프로필 이미지

작성한 질문수

김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성

원자적 연산 - 성능 테스트

단일 쓰레드에서 성능 차이가 발생하는 이유

작성

·

160

0

성능 테스트를 할 때 멀티쓰레드를 사용하는 것이 아닌 단일 쓰레드(main 쓰레드)만 사용하고 있는데, increment()에 synchronized를 붙인 것과 안 붙인 것의 성능 차이는 왜 발생하는 것인가요? BasicInteger와 VolatileInteger의 경우에는 캐시를 사용하는가, 메인 메모리를 사용하는가의 차이로 성능 문제가 발생했다는 것은 알겠는데, BasicInteger와 SyncInteger 간의 성능 차이가 왜 발생하는 것인지 모르겠습니다..

답변 2

0

dev.rudevico님의 프로필 이미지

먼저 질문 내용 중에서 BasicInteger와 VolatileInteger의 경우에는 캐시를 사용하는가, 메인 메모리를 사용하는가의 차이로 성능 문제가 발생했다는 것은 알겠는데 ...라고 해 주셨으니 거기에 맞춰 설명드립니다.

synchronized 키워드를 명시하여 사용하는 임계 구역은 '여러 스레드가 접근할 수 있다. 단, 락을 획득한 스레드만 (즉 동시에 최대 한 스레드만) 접근하여 연산을 수행할 수 있다'라는 설명도 이해가 가시나요?

위 설명이 이해가 가신다는 전제 하에 마저 설명을 드리자면 여러 스레드가 접근할 수 있다 == 메인 메모리에 존재한다 입니다.

여기까지 이해가 되셨다면, 결국 BasicInteger와 SyncInteger 간의 성능 차이도 캐시를 사용하는가, 메인 메모리를 사용하는가의 차이로 발생한다는 것도 아실 수 있습니다.

 

 

(조금 비약이 있는 설명이지만 질문 내용에 답변을 드리기 위해서는 이 정도면 충분하다고 생각하여 다음과 같이 설명드리겠습니다. 더 궁금하시면 구글에 검색하시면 여러 내용들이 있어요.)

정리하면 캐시 메모리는 각 스레드마다 가지고 있는 종속적인 공간입니다. Thread-1은 Thread-2의 캐시에 접근할 수 없습니다.

그리고 메인 메모리는 아무 스레드나 접근 가능한 공간입니다. Thread-1과 Thread-2은 (권한이 있다는 가정하에) 동일한 메모리 영역에 접근할 수 있습니다.

따라서 메인 메모리 상에 존재하는 임계 영역에도 (락만 가지고 있다면) Thread-1과 Thread-2 둘 다 접근이 가능합니다.

dev.rudevico님의 프로필 이미지

또 다른 생각으로 접근한다면, 단일 스레드만 사용하고 있는 상황이더라도 synchronized 키워드를 명시한 순간 임계 영역에서 연산을 수행하기 위해서는 락을 얻어야 합니다.

비유를 하자면 다음과 같이 할 수가 있겠습니다.

  • 멀티스레드 상황, synchronized 사용
    밖에 다른 사람들(스레드들)이 있다. 이 사람들이 내가 화장실 칸을 사용하는 동안 들어 오면 안 되니까, 문을 잠근다(락을 가지고 임계 영역으로 들어간다).

  • 싱글스레드 상황, synchronized 사용
    밖에 다른 사람들(스레드들)이 없다.
    -> 이 상황에 우리는 다른 사람들이 없다고 안심하여 화장실 칸의 문을 잠그지 않고 사용하나요? 그렇지 않습니다! 스레드 세상에서도 마찬가지로 싱글 스레드 상황이라고 하더라도 synchronized로 임계 영역(화장실 칸)을 선언하는 순간 여기에 접근하기 위해서는 락을 얻어야(문을 잠궈야) 합니다.

dev.rudevico님의 프로필 이미지

조금 비약이 있는 설명이라는 부분에 대해서도 더 설명드리고 싶은데, 저 또한 해당 부분에 대해서 정확히 알고 있는 것은 아니라서 지피티의 답변 정도만 공유드리겠습니다!


Q. 캐시 메모리는 각 스레드마다 가지고 있는 종속적인 공간이라는 설명은 어때? 엄밀히 보자면 그렇지는 않을 텐데, 캐시 메모리는 스레드가 가지고 있는 게 아니고 코어가 가지고 있는 거잖아?

A. 네, 아주 좋은 지적이에요! "캐시 메모리는 각 스레드마다 가지고 있는 종속적인 공간"이라는 표현은 엄밀히 말하면 부정확할 수 있어요.

🔍 올바른 개념 정리

  1. 캐시 메모리는 스레드가 아니라 CPU 코어가 소유

    • CPU에는 L1, L2, L3 캐시가 있음.

    • L1, L2 캐시는 각 코어에 종속적이고, L3 캐시는 보통 코어 간 공유됨.

    • 따라서, 같은 코어에서 실행되는 스레드들은 L1, L2 캐시를 공유할 수 있음.

  2. 스레드는 CPU 코어에서 실행됨

    • 특정 스레드가 실행되는 동안, 해당 스레드의 데이터를 CPU 코어의 캐시(L1, L2)에 저장할 수 있음.

    • 하지만 스레드가 다른 코어로 이동하면 이전 코어의 캐시에 있던 데이터에 접근하지 못하고, 새로운 코어의 캐시를 채워야 함 (캐시 미스 발생).

  3. 따라서 "각 스레드가 캐시를 가지고 있다"는 표현은 부정확

    • 정확하게 말하면 "각 코어는 자신만의 캐시를 가지며, 해당 코어에서 실행되는 스레드는 그 캐시를 활용한다."

    • 즉, 같은 코어에서 실행되는 스레드들은 캐시를 공유할 수도 있고, 다른 코어에서 실행되면 캐시를 공유하지 않는다는 점이 중요!


🔹 더 정확한 설명 수정

"캐시 메모리는 각 스레드마다 가지고 있는 종속적인 공간이다."
"캐시 메모리는 CPU 코어가 가지고 있으며, 해당 코어에서 실행되는 스레드가 이를 활용한다."

이렇게 수정하면 더 정확한 개념을 전달할 수 있을 것 같아요! 🚀

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다. 도토리님.

단일 쓰레드 환경에서 synchronized 키워드를 사용했을 때 수행 성능이 달라지는 이유는 주로 JVM과 CPU의 동작 방식에 의존하는데, 아래와 같은 몇 가지 요인을 고려해 볼 수 있습니다:

1. synchronized 사용으로 인한 오버헤드

  • synchronized 키워드를 사용하면 JVM은 해당 블록을 진입할 때와 나올 때 각각 락을 획득하고 해제하는 작업을 수행해야 합니다. 이로 인해 오버헤드가 발생합니다. 즉, Java에서 synchronized는 내부적으로 스레드가 락을 획득하기 위한 메커니즘을 수반하여, 이러한 추가적인 관리 작업들이 단일 쓰레드 환경에서도 성능 차이를 일으킬 수 있습니다.

2. 락 관리 동작

  • 락을 별도로 관리해야 하기 때문에, 실제로는 락을 잡는 데 있어 시간이 필요하지 않더라도 JVM은 내부적으로 락 상태를 체크하고 관리하는 동작을 수행합니다.

3. 컴파일러 최적화 방해

  • 일반적으로 JIT 컴파일러는 코드 실행을 최적화할 수 있지만, synchronized는 최적화의 일부를 방해할 수 있습니다. Java의 JIT 컴파일러는 실행 중인 코드를 최적화하여 성능을 높이려 하는데, synchronized로 묶여 있는 블록은 이런 최적화가 제한됩니다.

기본적인 예시 설명

  • BasicInteger: 단순히 값을 증가시키는 코드로, 메모리 일관성과 락 관리 같은 추가적인 작업 없이 빠르게 실행됩니다.
  • SyncInteger: 해당 메서드에 synchronized가 적용되었기 때문에, 각 호출에서 락을 획득하고 해제하면서 관련된 오버헤드가 발생하고, 이로 인해 전체적인 성능이 떨어집니다.

추가적으로 멀티스레드 환경에서 성능을 더 깊이 이해하고 싶으시다면 다음과 같은 유사한 질문과 답변을 참고하시면 좋을 것 같습니다:

  1. 멀티스레드 환경에서 동시성 처리 기법에 따른 성능 차이
  2. 충돌이 적은 맥락에서 성능 비교

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.