[F-Lab 66해빗 페이백 챌린지 ]

[F-Lab 모각코 챌린지 5일차] 쓰레드 안전, Final, Static

everydeveloper 2023. 5. 16. 21:39

오늘의 학습 목표

  • Thread Safety에 대해 설명 하시오 (키워드: lock, synchronized, deadlock, ThreadLocal, etc)
  • Static, Final 키워드에 대해 설명 하시오 (키워드: 사용이유, 장단점, etc)

TIL

 

 

 

Thread Safety

스레드 안전성(thread safety)은 여러 스레드가 동시에 코드의 특정 부분을 실행할 때 프로그램이 예상한 대로 동작한다는 것을 의미합니다. 스레드 안전성은 공유 데이터의 일관성을 유지하고, 예기치 않은 버그를 방지하기 위해 중요합니다.

여러 스레드가 동시에 같은 데이터에 액세스하려고 하면 데이터 무결성 문제가 발생할 수 있습니다. 예를 들어, 한 스레드가 데이터를 읽는 도중에 다른 스레드가 동일한 데이터를 수정하면, 첫 번째 스레드는 예상치 못한 결과를 얻을 수 있습니다. 이러한 상황을 방지하기 위해 동기화 메커니즘을 사용하여 해당 코드 부분에 대한 동시 액세스를 제한할 수 있습니다.

하지만 동기화는 오버헤드를 발생시키며 성능을 저하시킬 수 있습니다. 따라서 경우에 따라 ThreadLocal과 같은 도구를 사용하여 스레드 간 데이터 공유를 제한하고, 이로 인해 스레드 안전성을 보장할 수 있습니다.

 

Deadlock

데드락(deadlock)은 두 개 이상의 스레드가 서로의 작업이 끝나기를 무한히 기다리는 상태를 의미합니다. 이런 상황은 각 스레드가 서로에게 필요한 자원을 잠그고 있어서 발생합니다. 결과적으로 아무런 진행도 이루어지지 않고 시스템이 멈추게 됩니다.

예를 들어, 스레드 A가 자원 1을 잠그고 스레드 B가 자원 2를 잠근 상태에서, 스레드 A가 자원 2를 요청하고, 스레드 B가 자원 1을 요청하는 상황을 생각해봅시다. 이 경우 두 스레드 모두 다른 스레드가 자신이 필요로 하는 자원을 해제할 때까지 기다리게 되므로 데드락 상태에 빠집니다.

데드락을 방지하는 방법은 여러 가지가 있습니다. 예를 들어, 자원에 대한 액세스를 순차적으로 제한하거나, 한 번에 여러 자원을 요청하는 대신 한 번에 하나씩만 요청하도록 하는 등의 방법이 있습니다. 또한, 타임아웃을 설정하여 특정 시간이 지나면 자원 요청을 중단하고 다시 시도하게 할 수 도 있습니다. 이렇게 하면 무한히 기다리는 상황을 방지할 수 있습니다.

또 다른 대안은 "사전 조건"을 설정하는 것입니다. 즉, 스레드가 자원을 요청하기 전에 특정 조건이 충족되었는지 확인합니다. 예를 들어, 스레드가 자원을 잠글 수 있는지 먼저 확인하고, 그렇지 않은 경우 요청을 지연하거나 다른 작업을 수행합니다.

데드락 문제는 복잡하고 예측하기 어려울 수 있으며, 디버깅도 어렵습니다. 따라서 소프트웨어를 설계하고 구현할 때 이러한 문제를 미리 고려하여 데드락을 최대한 피하도록 하는 것이 중요합니다.

데드락 예방을 위한 일반적인 전략은 다음과 같습니다:

  1. 상호 배제(Mutual Exclusion) 방지: 공유 자원에 대한 동시 액세스를 허용하도록 설계합니다. 이는 항상 가능한 것은 아닙니다.
  2. 점유 및 대기(Hold and Wait) 방지: 프로세스가 자원을 요청할 때 이미 다른 자원을 보유하고 있지 않아야 합니다. 즉, 한 번에 모든 필요한 자원을 요청해야 합니다.
  3. 비선점(No Preemption) 방지: 자원이 다른 프로세스에 의해 점유되어 있을 때, OS가 해당 자원을 선점하고 요청한 프로세스에게 할당할 수 있어야 합니다.
  4. 환형 대기(Circular Wait) 방지: 자원을 순차적으로 번호를 매기고, 각 프로세스는 번호가 다음에 오는 자원만을 요청하도록 합니다. 이 방법은 환형 대기 상태를 생성하지 않습니다.

이러한 전략들은 데드락을 예방하거나 피하는 데 도움이 될 수 있습니다. 하지만 각 전략이 모든 상황에 적합한 것은 아니므로, 각각의 상황에 맞는 적절한 방법을 선택해야 합니다.

 

Static

Java에서 static 키워드는 클래스 레벨에서 사용되는 것을 나타냅니다. static 변수는 클래스의 모든 인스턴스가 공유하는 하나의 변수입니다. 이는 클래스 변수라고도 불립니다.

 

Final

Java에서 final 키워드는 한 번 할당되면 변경할 수 없는 것을 나타냅니다.

 

Static 사용 이유

  • 공유 목적: static 변수는 클래스의 모든 인스턴스가 공유합니다. 이를 통해 모든 인스턴스가 동일한 데이터에 액세스할 수 있도록 하거나, 데이터를 중복해서 저장하는 것을 방지할 수 있습니다.
  • 유틸리티 함수: 특정 클래스와 관련이 있지만 인스턴스 상태에 의존하지 않는 메소드는 static으로 선언할 수 있습니다. 이러한 메소드는 보통 유틸리티 함수로 사용됩니다. 예를 들어, Math 클래스의 abs나 sqrt 함수와 같은 함수들은 static입니다.
  • 메모리 효율성: static 변수나 메소드는 클래스 당 한 번만 메모리에 로드되므로, 메모리 사용을 최소화하는 데 도움이 됩니다.

Final 사용 이유

  • 불변성 보장: final 변수는 한 번 초기화되면 그 값을 바꿀 수 없습니다. 이 특성은 불변 객체를 만들 때 유용하며, 불변 객체는 멀티스레드 환경에서 안전하게 공유할 수 있습니다.
  • 보안: final 클래스나 메소드는 하위 클래스에서 확장하거나 재정의할 수 없습니다. 이 특성은 클래스나 메소드의 동작을 보호하고, 잘못된 사용으로부터 보호하는 데 사용됩니다.
  • 최적화: 컴파일러는 final 변수의 값을 알고 있으므로, 더 효율적인 코드를 생성하는 데 활용할 수 있습니다.

메모리 오버헤드

메모리 오버헤드는 프로그램이 실제 데이터 처리를 위해 필요한 것보다 더 많은 메모리를 사용하는 현상을 말합니다. 이는 일반적으로 메타데이터 관리, 메모리 할당 및 해제, 가비지 컬렉션과 같은 시스템 동작이나 프로그램의 특정 구조에 의해 발생합니다.

예를 들어, 객체 지향 프로그래밍에서 객체는 메소드와 멤버 변수를 가지고 있습니다. 이 객체들은 실제 데이터 외에도 이런 정보를 저장해야 하므로 추가적인 메모리를 차지합니다. 이런 추가적인 메모리 사용량이 메모리 오버헤드를 구성합니다.

메모리 오버헤드는 특히 메모리가 제한적인 시스템에서 문제가 될 수 있습니다. 심한 경우에는 메모리 부족으로 프로그램이 비정상적으로 종료되는 등의 문제를 일으킬 수 있습니다. 따라서 소프트웨어를 설계하고 구현할 때는 메모리 오버헤드를 최소화하는 것이 중요합니다. 이를 위해 메모리 효율적인 자료구조 선택, 객체 재사용, 메모리 누수 방지 등의 방법을 사용할 수 있습니다.

 

자바 Execution과정 및 Interpreter / JIT Compiler의 역할

  1. 소스 코드 작성: 프로그래머는 .java 확장자를 가진 파일에 Java 언어로 소스 코드를 작성합니다.
  2. 컴파일: javac 컴파일러는 소스 코드를 읽어들여, Java 바이트코드로 변환합니다. 이 바이트코드는 .class 확장자를 가진 파일에 저장됩니다.
  3. 실행: java 실행 엔진은 .class 파일을 읽어들여, 해당 바이트코드를 실행합니다. 이때 Java 실행 엔진은 인터프리터와 JIT 컴파일러를 활용하여 바이트코드를 실행합니다.

이 중에서 인터프리터와 JIT(Just-In-Time) 컴파일러는 Java 실행 엔진의 핵심 구성요소입니다.

 

Interpreter

인터프리터는 바이트코드를 한 줄씩 읽어들여 해당 명령을 실행합니다. 인터프리터의 장점은 간단한 구조와 빠른 시작 시간입니다. 하지만 한 줄씩 실행하므로 반복되는 코드나 루프에서는 효율성이 떨어질 수 있습니다.

 

JIT Compiler

JIT 컴파일러는 반복되는 코드나 빈번하게 호출되는 메소드를 발견하면, 해당 부분의 바이트코드를 네이티브 코드로 컴파일합니다. 이렇게 컴파일된 코드는 훨씬 빠르게 실행될 수 있습니다. 이후에는 컴파일된 네이티브 코드를 직접 실행하므로, 인터프리터가 매번 바이트코드를 해석하는 것보다 효율적입니다.

따라서 인터프리터와 JIT 컴파일러는 각각의 장점을 살려 Java의 실행 성능을 최적화합니다. 인터프리터는 시작 시간을 단축시키고, JIT 컴파일러는 실행 시간을 줄입니다. 이 둘은 함께 작동하여 Java 프로그램의 전반적인 성능을 향상시킵니다.

 

SynchronizedCocurrent가 앞에 붙는 것들은 왜 만들어졌고 다른점은 무엇인지 이해시켜줘

 

  1. Synchronized
    synchronized 키워드는 Java에서 동기화를 제공하는 가장 기본적인 방법입니다. 키워드는 특정 객체에 대한 동시 접근을 제한하여 번에 하나의 스레드만이 해당 코드 블록을 실행하도록 합니다.
    위의 코드에서 increment 메소드는 synchronized 키워드로 인해 번에 하나의 스레드만이 접근할 있습니다. 이로 인해 count 변수의 값을 안전하게 증가시킬 있습니다.
    그러나 synchronized 키워드는 여러 단점이 있습니다. 예를 들어, 동시에 수행할 있는 작업의 수를 제한하므로 성능 저하를 일으킬 있습니다. 또한, 잘못 사용하면 교착상태(데드락) 일으킬 가능성이 있습니다.
  2. Concurrent Classes
    Concurrent 클래스들은 java.util.concurrent 패키지에 포함되어 있으며, 고급 동시성 제어를 제공합니다. 클래스들은 synchronized 키워드의 단점을 극복하고, 개발자가 복잡한 동시성 문제를 쉽게 처리할 있도록 돕습니다.
    예를 들어, ConcurrentHashMap 동시성 해시 맵을 제공하며, synchronizedMap보다 높은 동시성을 제공합니다. 또한, ExecutorService, Semaphore, CyclicBarrier 등의 클래스는 복잡한 스레드 관리와 동기화 작업을 보다 쉽게 처리할 있게 돕습니다.

결국 synchronized 키워드와 Concurrent 클래스들은 모두 멀티스레딩 환경에서 데이터의 일관성을 유지하고 동시성 문제를 해결하는 데 사용되지만, 사용법과 성능, 그리고 제공하는 기능의 차이가 있습니다. 따라서 상황에 따라 적절하게 선택하여 사용해야 합니다.