아이템 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
라는 유틸리티를 사용해야한다.
'개발 공부 > Java' 카테고리의 다른 글
이펙티브 자바 아이템 18 - 상속보다는 컴포지션을 사용하라 - 완벽 공략 (0) | 2022.11.24 |
---|---|
이펙티브 자바 아이템 18 - 상속보다는 컴포지션을 사용하라 - 핵심 정리 (0) | 2022.11.22 |
이펙티브 자바 아이템 17 - 변경 가능성을 최소화 하라 - 핵심 정리 (0) | 2022.11.21 |
이펙티브 자바 아이템 16 - public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 - 완벽 공략 (1) | 2022.11.14 |
이펙티브 자바 아이템 16 - public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 - 핵심 정리 (0) | 2022.11.14 |
댓글