여러 스레드가 실행 중이면 운영체제의 스레드 스케줄러가 어떤 스레드를 얼마나 오래 실행할지 정한다. OS 별로 구체적인 스케줄링이 다를 수 있고, 이러한 정책에 좌지우지되지 않는 프로그램이 좋은 프로그램이라고 볼 수 있다. 이식성이 좋은 프로그램은 뭘까? 정확성이나 성능이 스레드 스케줄러에 따라 달라진다면 다른 플랫폼(OS)에 이식하기 어렵다. 실행 가능한 스레드 수와 생명 주기 실행 중인(혹은 가능한) 스레드 수 = 전체 스레드 수 - 대기 중인 스레드 수 실행 가능한 스레드의 평균적인 수를 생각한다. 이식성이 좋은 프로그램을 작성하기 위해서는 실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많지 않도록 하는 것이 중요하다. 이렇게 해야 스레드 스케줄러가 고민할 거리가 줄어든다. 실행 준비..
일일 커밋 회고 들어가며, 누군가 내게 꾸준히 몰두해온 경험을 묻는다면 자신 있게 일일 커밋이라 말할 것이다. 개발자라면 누구나 멋진 학습 방법을 찾고 싶어 한다. 나에게 일일 커밋은 가장 멋진 학습 방법이었다. 2020년 10월부터 시작해 1년 조금 넘는 시간 동안 꾸준히 했으니, 나름의 노하우와 자부심(?)도 생겼다. 그래서 이번 기회에 일일 커밋의 간단한 회고와 내가 진행한 방법들을 공유하는 시간을 가져볼까 한다. 시작한 이유🤔 개발자로의 새싹 시절, 나에게 맞는 학습 방법을 찾기 위해 많은 고민을 했었다. 학습 루틴 기록이란 글에서 언급했 듯, 일일 커밋도 도전해 보고 싶은 학습 방법 중 하나였다. 그러던 중 동료 개발자들과 스터디를 진행하게 되면서 자연스럽게 개인 저장소에 커밋을 하게 되었다. ..
지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다. 지연 초기화는 주로 최적화 용도로 쓰이며, 클래스와 인스턴스 초기화 시 발생하는 위험한 순환 문제를 해결하는 효과도 있다. 지연 초기화의 특징 지연 초기화는 인스턴스 생성 시 초기화 비용은 줄지만, 그 필드에 접근하는 비용은 커진다. 따라서 초기화가 이루어지는 비율, 실제 초기화에 드는 비용, 호출 빈도에 따라 오히려 성능이 느려질 수 있다. 인스턴스의 사용 빈도가 낮지만 초기화 비용이 크다면 지연 초기화가 제 역할을 해준다. 멀티 스레드 환경에서 지연 초기화 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면, 반드시 동기화해야 한다. 그렇지 않으면 심각한 버그로 이어질 수 있다. (item78 - 공유 중인 가변 데이..
여러 스레드가 하나의 메서드를 동시에 호출할 때 그 메서드가 어떻게 동작하느냐는 클라이언트와 클래스 간의 중요한 계약과도 같다. 만약 API 문서에 아무런 언급이 없다면 사용자는 나름의 가정을 하고 사용해야 한다. 만약 이 가정이 틀린다면 프로그램은 심각한 오류로 이어질 수 있다. API 문서에 synchronized 한정자 synchronized 한정자가 달려있는 메서드는 스레드 안전하다고 생각할 수 있다. 하지만 이는 몇 가지면에서 틀렸다. 메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일 뿐 API에 속하지 않는다. 따라서 이것만으로는 해당 메서드가 스레드 안전하다고 믿기 어렵다. 멀티스레드 환경에서도 API를 안전하게 사용하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명..
고수준의 동시성 유틸리티가 Java5에서 도입되면서 wait와 notify를 사용해야 할 이유가 많이 줄었다. wait와 notify는 올바르게 사용하기 아주 까다로우니 고수준 동시성 유틸리티를 사용하자. 동시성 유틸리티 java.util.concurrent 동시성 유틸리티는 실행자 프레임워크, 동시성 컬렉션, 동기화 장치 이렇게 세 범주로 나눌 수 있다. 실행자 프레임워크 앞선 item80에서 실행자 프레임워크에 대한 내용을 다뤘다. 이 내용에 대한 내용은 item80을 참고하자. 동시성 컬렉션 List, Queue, Map 등 표준 컬렉션 인터페이스에 동시성을 추가해 구현한 고성능 컬렉션이다. 동기화를 내부에서 수행하여 높은 동시성에 도달할 수 있다. 내부에서 동기화를 수행하므로 동시성을 무력화할 수..
과거에는 클라이언트가 요청한 작업을 백그라운드 스레드에 위임해 비동기적으로 처리하기 위해 단순한 작업 큐를 사용했다. 하지만 이는 안전 실패나 응답 불가에 대한 예외 코드 작성 등으로 수많은 코드 작업이 발생했다. 다행히도 실행자 프레임워크의 등장으로 이제 이러한 수고를 덜 수 있게 되었다. 실행자 프레임워크(Executor Framework) 실행자 프레임워크는 java.util.concurrent 패키지에 속해있으며, 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다. 기존보다 모든 면에서 뛰어난 작업 큐를 이제는 단 한 줄로 생성할 수 있다. // 작업 큐 생성 ExecutorService exec = Executors.newSingleThreadExecutor(); // 실행할 태스크를 넘김..
과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠드리고, 예측할 수 없는 동작을 낳기도 한다. 응답 불가(교착 상태)와 안전 실패(데이터 훼손)를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대 클라이언트에 양도하면 안 된다. 외계인 메서드 응답 불가와 안전 실패를 유발할 수 있는 메서드 즉, 동기화된 영역에서 재정의할 수 있는 메서드 혹은 클라이언트가 넘겨준 함수 객체 등을 동기화된 클래스 관점에서 외계인 메서드라고 한다. 동기화된 클래스는 외계인 메서드가 무슨 일을 할지 알지 못하며 통제할 수도 없고 외계인 메서드가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착상태에 빠지거나, 데이터를 훼손할 수도 있다. 외계인 메서드 예제 ForwardingSet class Forwarding..
동기화(synchronized) synchronized(동기화) 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다. 1. 동기화에 대한 오해 많은 프로그래머가 동기화를 배타적 실행을 막는 용도로만 생각한다. 즉, 한 스레드가 변경하는 중이라 상태가 일관되지 않은 순간의 객체를 다른 스레드가 접근하지 못하도록하는 용도로만 생각한다. 하지만 동기화에는 중요한 기능이 하나 더 있다. 동기화된 메서드나 블록에 들어간 스레드가 같은 락(lock)의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다. 2. 동기화가 필요한 이유 Java 언어 명세에는 "long과 double 외의 변수를 읽고 쓰는 동작은 원자적이다."라고 명세되어있다. 이는 항상 어떤 스레드가 정상적으로 저장한 값을 온..
API 설계자가 메서드 선언에 예외를 명시하는 이유는, 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것이다. 하지만 사람들이 자주 어기고 흘려버리는 경우가 많다. 예외를 무시하는 방법은 아주 쉽다. 단순히 메서드 호출을 try-catch로 감싼 후 catch에서 아무 일도 하지 않으면 된다. // catch를 비워두면 예외는 무시된다. try { ...... } catch(SomeException e) { // 아무일도 하지 않는다. } catch 블록을 비워두지 말자 예외는 문제 상황에 대처하기 위해 존재하는데 catch 블록을 비워두면 예외가 존재할 이유가 사라진다. 비유하자면 화재경보를 무시하는 수준이 아니라 아예 꺼버려, 다른 누구도 화재가 발생했는지 모르게하는 것과 같다. 이러한 상황..