본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 17 - 변경 가능성을 최소화 하라 - 완벽 공략

by 개발인생 2022. 11. 21.
반응형

아이템 17 - 변경 가능성을 최소화 하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

새로 생성된 불변 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작

불변 인스턴스는 synchronized 와 같은 동기화 작업을 할 필요없이 다른 스레드에서 공유해서 써도 안전하다.

final 과 자바 메모리 모델(JMM)

final 은 해당 변수가 초기화가 되면 다른 값으로 바뀌지 않게 해주는 것이다.

상수로 만들때 주로 사용한다.

final 을 사용하면 해당하는 필드값을 안전하게 초기화 할 수 있다.

이를 이해하려면 자바 메모리 모델에 대한 개념과 자바 메모리 모델에서 final 이 어떻게 동작하는지 이해해야한다.

자바 메모리 모델은 JVM 의 메모리 구조를 말하는 것이 아니다.

JMM 은 어떤 주어진 프로그램을 실행하는 과정이 적합한지 알려주는 것이다.

즉, 프로그램을 어떻게 실행할 것인지에 대한 룰을 정한 것이다.

어떤 객체에 값을 할당하는 작업이 있다고 가정할 때 객체에 값을 할당하는 순서는 바뀔수도 있다.

실행 순서를 어떻게 할지는 구현체의 자유이다.

대신 이 실행순서는 자바 메모리 모델 이 허용하는 범위 내에서 어떻게 실행할지를 정한다.

자바 메모리 모델 은 어떻게 실행해도 괜찮은지에 대한 규칙 같은 것이다.

때문에 우리가 직관적으로 생각하는 순서와는 다르게 프로그램 실생 순서가 바뀌어서 실행되는 경우가 있을 수 있다.

public class Whiteship {

    private final int x;

    private final int y;

    public Whiteship() {
        this.x = 1;
        this.y = 2;
    }

    public static void main(String[] args) {
        Whiteship whiteship = new Whiteship();
    }
}

위의 코드에서 우리가 생각되어 지는 과정은

    public static void main(String[] args) {
        // Object w = new Whiteship() 1. 인스턴스 생성 
        // w.x = 1  2. 값 할당
        // w.y = 2
        // whiteship = w 3. 레퍼런스 할당

        Whiteship whiteship = new Whiteship();
    }

위의 과정처럼 순서가 진행될 것 처럼 보인다.

    public static void main(String[] args) {
        // Object w = new Whiteship()   1. 인스턴스 생성 
        // whiteship = w   2. 레퍼런스 할당
        // w.x = 1    3. 값 할당
        // w.y = 2

        Whiteship whiteship = new Whiteship();
    }

하지만 실행순서는 메모리 모델이 허용하는 범위 내에서 다를 수 있기 때문에 위처럼 실행될수도 있다.

메모리 모델은 해당 실행 순서가 유효한지 아닌지에 대해 한 스레드 내에서만 판단한다.

멀티 스레드 환경에 대해서는 계산하지 않는다.

때문에 실행순서가 어떻게 바뀌느냐에 따라 멀티 스레드 환경 내에서 값이 할당되기 전에 값을 참조하는 경우가 있을 수 있다.

멀티 스레드 환경에서 불안정한 초기화 가 발생할 수도 있다.

final 은 해당하는 필드가 초기화 된 이후에만 해당 값을 사용할 수 있다.

어떤 인스턴스의 final 변수를 초기화 하기 전까지 해당 인스턴스를 참조하는 모든 스레드는 기다려야 한다.

class FinalFieldExample { 
    final int x;
    int y; 
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3; // final 이기 때문에 값이 할당되기 전까지 다른 곳에서 참조 불가
        y = 4; 
    } 

    static void writer() {
        f = new FinalFieldExample();
    } 

    static void reader() {
        if (f != null) {
            int i = f.x;  // guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

다만 final 필드의 값이 할당된 이후에는 다른 값을 안전하게 참조하는 걸 보장하지 않는다.

반듯이 초기화가 된 이후에 사용되어야 할 값들은 final 키워드를 사용해야한다.


java.util.concurrent 패키지

concurrent 패키지는 병행 프로그래밍 또는 병렬 프로그래밍에 유용하게 사용할 수 있는 유틸리티를 제공하는 패키지이다.

병행 프로그래밍(Concurrency) 같은 경우는 하나의 CPU 를 시분할해 여러작업을 동시에 하는 것처럼 보이지만

한순간에 하나의 작업만 진행하는 방식이다.

병렬 프로그래밍(Parallelism) 실제로 나란히 동시에 작업한다.

때문에 병렬 프로그래밍(Parallelism) 에서는 멀티 코어 CPU 가 필요하다.

병행 프로그래밍(Concurrency)병렬 프로그래밍(Parallelism) 같이 쓰일수도 있다.

실제로 자바가 제공하는 스레드라는 개념을 활용하고 스레드를 여러개 사용하게 되면

해당하는 스레드들은 멀티 코어 컴퓨터를 사용하고 있다면 여러개의 CPU 에 적절하게 배분이 되서 모든 코어를 사용하며 CPU 를 사용하게 된다.

병행 이면서 병렬적인 작업을 처리해준다.

이게 JVM 의 큰 장점 중 하나이다.

때문에 우리는 여러 스레드를 사용했을 때 스레드들이 동시에 변수를 참조할 때 발생할 수 있는 문제,

스레드 간의 경쟁(A 스레드가 먼저 실행되냐 B 스레드가 먼저 실행되냐) 에 따라 값이 달라지는 레이스 컨디션을 고려해야한다.

concurrent 패키지는 이러한 여러가지 문제를 방지할 수 있는 툴을 제공해준다.

CountDownLatch

CountDownLatch 는 다른 여러 스레드로 실행하는 여러 오퍼레이션이 마칠 때 까지 기다릴 때 사용할 수 있는 클래스이다.

public class ConcurrentExample {
    public static void main(String[] args) throws InterruptedException {
        int N = 10;
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(N);

        for (int i = 0; i < N; ++i) // create and start threads
            new Thread(new Worker(startSignal, doneSignal)).start();

        ready();            // don't let run yet
        startSignal.countDown();      // let all threads proceed
        doneSignal.await();           // wait for all to finish
        done();
    }

    private static void ready() {
        System.out.println("준비~~~");
    }

    private static void done() {
        System.out.println("끝!");
    }

    private static class Worker implements Runnable {

        private final CountDownLatch startSignal;
        private final CountDownLatch doneSignal;

        public Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
            this.startSignal = startSignal;
            this.doneSignal = doneSignal;
        }

        public void run() {
            try {
                startSignal.await();
                doWork();
                doneSignal.countDown();
            } catch (InterruptedException ex) {} // return;
        }

        void doWork() {
            System.out.println("working thread: " + Thread.currentThread().getName());
        }
    }
}

CountDownLatch 는 초기화 할 때 숫자를 입력하고, await() 메서드를 사용해서 숫자가 0이 될때까지 기다린다.

CountDownLatch 는 시작 또는 종료 의 신호로 사용할 수 있다.

여러 스레드들을 사용할 때 해당 스레드들을 기다렸다가 작업을 해야하는 경우 유용하게 사용할 수 있다.

CountDownLatch 는 재사용 할 수 있는 인스턴스가 아니다.

만약 재사용하고 싶다면 CyclicBarrier 라는 유틸리티를 사용해야한다.

반응형

댓글