지난, 2장에서는 공유하는 데이터를 여러 곳에서 사용할 때 동기화하는 방법을 알아봤다.
이번 3장에서는 여러 스레드에서 특정 객체를 동시에 사용할 때 섞이지 않고 안전하게 동작하도록 객체를 공유하고 공개하는 방법을 알아볼 계획이다.
가시성(Visibility)
메모리 가시성이란, 멀티 스레드 환경에서 특정 공유변수를 참조하는 여러 스레드가 있을 때 하나의 스레드에서 데이터를 변경했는지 여부를 알 수 있느냐의 문제를 이야기한다.
(동기화를 파악할 수 있느냐?)
이러한 가시성 문제는 직관적으로 이해할 수 있는 문제가 아니기에 쉽게 파악하고 코드를 작성하기 어렵다.
아래 코드를 보자.
public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
위 코드는 동기화를 사용하지 않은 코드이다.
main 스레드가 실행되지 않는다면, 평생 읽기 스레드에서 number를 출력하지 못할 수도 있다. 또는, 읽기 스레드가 메인스레드에서 지정한 number값보다 먼저 number값을 읽을 수도 있는, 소스코드가 100%로 코딩된 순서로 작동하지 않는 `재배치 문제`가 발생할 수도 있다.
재배치 문제란?
package programmers;
public class Solution {
private int x = 0;
private int y = 50;
public void write(){
x=100;
y=50;
}
public void read(){
if(x<y)
System.out.println("read");
}
public static void main(String[] args) {
final Solution solution = new Solution();
new Thread(){
public void run(){
solution.write();
}
}.start();
new Thread(){
public void run(){
solution.read();
}
}.start();
}
}
동기화를 명시적으로 하지 않으면 최적화를 위해 JVM에서 의존관계가 없는 명령어의 실행 순서를 바꿈.
가령 x=100,y=50 두 명령어는 의존관계가 없으므로, 랜덤하게 두 명령어의 순서가 바뀔 수 있고 그 경우에, read 메서드의 조건을 만족하는 경우가 생길 수 있음.
이처럼, 동기화 기능을 지정하지 않으면 컴파일러, 프로세서, JVM등 프로그램 코드가 실행되는 순서를 임의로 바꿔 실행하는 이상한 경우가 발생할 수 있다. 따라서, 동기화 되지 않은 상황에서 메모리상의 변수를 대상으로 작성한 코드가 순서대로 일어날 것이란 보장은 없다.
즉, 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 사용해야한다.
스테일 데이터
스테일(stale) 데이터란, 여러 스레드가 공유 변수에 접근하는 상황에서 값을 읽을 때 데이터가 최신화가 되어있지 않을 수도 있는 데이터다. 이 경우의 문제는 정상적으로 코드가 작동될 수도, 비정상적으로 작동될 수도 또 영원히 종료되지 않을 수도 있다는 것이다. 같은 코드임에도 실행시점과 상황에 따라서 결과가 달라진다라... 쉽지 않다. 올바르지 않은 값을 출력할 수도, 자원을 계속 점유할 수도 있다.
다음 코드 역시 동기화가 되어있지 않은 Mutable 클래스이다.
public class MutableInteger{
private int value;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
}
여러 스레드에서 get,set을 사용할때 set한시점의 내용이 반영되지 않고 get을 한다면 최신화되지 않은 값을 불러올 수 있는 것이다.
따라서, 이를 해결하기 위해선 `동기화`를 통해 해결해야한다.
public class SynchronizedInteger{
@GuardedBy("this")
private int value;
public synchronized int get(){
return value;
}
public synchronized void set(int value){
this.value = value;
}
}
단일하지 않은 64비트 연산
동기화되지 않은 상황에서 값을 읽을땐, 스테일 상태의 값을 읽을 가능성이 있지만 전혀 근거없는 값을 읽지는 않는다. 대부분 바로 이전에 다른 스레드에서 설정한 값을 가져가게 된다.
그러나, 64비트를 사용하는 double, long 형 타입에서는 난데 없는 값이 생길 수 있다. 자바 메모리 모델은 값을 가져오고 저장하는 연산이 단일해야한다고 정의하지만, `volatile`을 사용하지 않는 64비트 값을 읽거나 쓸때는 32비트 연산을 두번씩 해서 사용하도록 정의한다. 이러한 long, double 변수를 여러 스레드에서 사용하는 경우가 있다면 문제가 발생할 수 있다.
락과 가시성
값을 변경할 수 있는 변수를 여러개의 스레드에서 동시에 사용한다면, 바로 이전 스레드에서 사용했던 변수의 값을 오류없이 정상적으로 다음 스레드가 사용할 수 있게 하기 위해 동일한 락을 사용하는 synchronized 블록으로 막아줄 필요가 있다. synchronized해서 가시성을 확보하기 위한 락을 활용하는 것이다. 여러 스레드가 공유해 사용하는 변수를 각 스레드에서 각자 최신의 값으로 활용하려면 동일한 락을 사용해 모두 동기화 시켜야한다.
Volatile 변수
volatile이란 휘발성이 있는, 이란 뜻으로 자바에선 조금 더 약한 동기화 기능을 제공하는 것을 의미한다. volatile 키워드를 사용하면, 이 변수는 공유변수이고 따라서 실행 순서를 재배치하면 안된다라는 의미를 갖는다. 이 변수는 캐싱이 되지 않아 매번 새로운 값을 읽어올 수 있다.
말 그대로 특정 아무런 락을 사용하지 않는 약한 동기화기능을 제공하기에, synchronized와 같이 특정 스레드 A가 값을 쓰고 B가 이를 읽을때 A가 변수에 쓰기전에 있었던 모든 값을 B에서도 볼 수 있다.
그러나, 메모리 가시성을 확보하기 위한 정도일뿐 synchronized보다 약한 동기화를 준다.
따라서, 동기화하고자 하는 부분을 명확히 볼 수 있고, 구현하기가 훨씬 간단한 경우에만 volatile 변수를 사용하자.
volatile boolean asleep;
while(!asleep)
countSomeSheep();
이 경우 락을 걸어때와 같이 변경사항이 발생할때 확인을 할 수 있지만, asleep과 같이 작업을 완료했을때, 인터럽트가 걸릴때와 같은 플래그 변수에 volatile을 지정하는데 하나의 스레드에서만 사용한다는 보장이 없다면 `count++`과 같은 증가 연산자를 사용한 부분까지 동기화를 맞춰주지는 않는다.
즉, 락을 사용하면 가시성과 단일성을 확보할 수 있지만 volatile변수는 가시성만 확보할뿐 단일성은 확보할 수 없다.
따라서, volatile 변수는 다음과 같은 경우에서 사용할 수 있다.
- 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나 해당 변수의 값을 변경하는 스레드가 하나만 존재
- 해당 변수가 객체의 불변조건을 이루는 다른 변수와 달리 불변 조건에 관련되어 있지 않을때
- 해당 변수를 사용하는 동안 어떤 경우라도 락을 걸어둘 필요가 없는 경우.
공개와 유출
특정 객체를 현재 코드의 스코프밖에서 사용할 수 있도록 만들면 public 공개된 상태라고 한다. 일반적으로 내부의 상태는 공개하지 않지만 특정 객체를 공개해 여러 부분 공유해서 사용하도록 하는데 이때 반드시 동기화 과정을 거쳐야한다. 안정적이지 않은 상태에서 공개하면 스레드 안전성에 문제가 생길 수 있으며, 이처럼 의도치 않게 외부에서 사용할 수 있도록 공개된 경우를 `escaped` 유출 상태라고 한다.
public static을 통한 공개
자바 프로그램에선 아래 코드와 같이 public static을 통해 직접 적인 방법으로 해당 객체를 모든 클래스, 모든 스레드에서 사용할 수 있도록 공개할 수 있다.
public static Set<Secret> knownSecrets;
public void initialize(){
knwonSecrets = new HashSet<Secret>();
}
특정 객체 하나를 공개해도 그와 관련된 다른 객체까지 덩달아 공개될 수 있다. 우선, 위 코드 Secret에서 인스턴스 하나를 추가한다면, 추가한 Secret 인스턴스도 함께 공개된다.
private으로 선언된 내부 인스턴스 공개
class UnsafeStates {
private String[] states = new String[] {"AK", "AL" ...};
public String[] getStates() {
return states;
}
}
private 변수인 states가 getter 메서드를 통해서 조회될 수 있다. 따라서, 이 경우에 states 변수는 유출상태라고 할 수 있다.
또 다음 코드를 보자. 내부클래스의 인스턴스를 외부에 공개하는 경우로, 내부 클래스는 항상 부모클래스에 대한 참조를 갖고 있기에, ThisEscape 클래스가 EvenetListener 객체를 외부에 공개하면 EventListener 클래스를 포함하는 ThisEscape 클래스도 모두 외부에 공개된다.
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
생성 메서드의 안전성
생성 메서드를 실행하는 도중 this 변수가 외부에 유출될 수 있다. 위 코드의 EventListener의 내부 클래스가 공개되었기에, 이를 감싸는 ThisEscape 클래스도 함께 공개됐다. 일반적으로 생성 메소드가 완전히 종료된 이후 초기화가 진행되기에, 생성자 실행도중에 상태가 외부에 공개되면 정상적이지 않은 값을 참조할 수 있다.
따라서, 생성메서드를 실행하는 도중 this 변수가 외부에 유출되지 않게 해야한다.
이를 해결하기 위해선 아래와 같이 생성메서드를 private으로 지정하고 publicㅇ로 지정된 팩토리 메소드를 만들어 사용할 수 있다.
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
스레드 한정
변경 가능한 객체를 공유해 사용한다면 항상 동기화를 해야한다. 그렇지 않을 경우 객체를 공유하지 않아야하는데, 특정 객체가 단일 스레드에서 사용되는 것이 확실하다면 동기화를 할 필요가 없다. 스레드 안전하지 않은 객체를 스레드 한정을 통해 해결하는 것이다. 락과 스레드 한정 기능을 언어적인 차원에서 제공하지 않기에, 스레드 한정 기법은 프로그램을 설계하는 과정부터 고려해야한다.
이러한 방법을 사용하는 예로는 Swing, JDBC 등이있다
JDBC
Connection 객체가 스레드 안전성을 반드시 확보하고 있어야하는 것은 아닌데, Connection 풀에서 DB연결을 확보하고, 확보한 연결로 요청 하나를 처리하고 반환하는 과정을 거친다. 이 풀에서 하나를 사용 중이면 해당 연결을 다른 스레드에서는 사용하지 못하게 설계하는 단일 스레드의 형태를 띈다.
스레드 한정 - 주먹 구구식
스레드 한정 기법을 구현 단계에서 알아서 잘 처리할 경우가 있는데 이때 임시 방편으로 스레드 한정 기법을 적용할 수 있다. 정석적인 방법은 아니고 임시 방편으로 에러가 발생할 가능성이 높다. 가령 특정 스레드에 한정하려는 객체가 volatile로 선언되어있다면, 특정 단일 스레드에서만 쓰기작업을 가능하게 해 스레드를 한정할 수 있다.
스택 한정
특정 객체를 로컬 변수를 통해서만 사용할 수 있는 특별한 경우의 스레드 한정 기법을 스택 한정 기법이라고 한다. 특정 클래스 내부에 숨겨둔 변수를 특정 스레드에 한정시키는 것이다. 로컬 변수는 현재 실행중인 스레드에 한정되어 있다고 볼 수 있고 즉, 실행중인 스레드 내부의 스택만 존재하기에 외부 스레드에선 볼 수 없다.
이 방법은 스레드 한정 기법보다 안전하다.
ThreadLocal
스레드 내부의 값과 값을 가지고 있는 객체를 연결해 스레드 한정 기법을 적용할 수 있다. ThreadLocal 클래스에 존재하는 get, set 메소드를 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리해줘 다시 말해 ThreadLocal 클래스의 get메서드를 호출하면 현재 진행중인 스레드의 최근 set 메소드를 호출했던 값을 가져오는 것이다. ThreadLocal 변수는 변경 가능한 싱글톤, 전역 변수를 기반으로 설계되어있는 구조에서 변수가 임의로 공유되는 상황을 막기 위해 사용하는 경우가 많다.
JDBC에선 이러한 방법을 많이 활용하는데 자주 호출하는 메서드에 임시 버퍼와 같은 객체를 만들고, 이미 만들어진 객체를 재활용하고자 할 때 많이 사용한다.
불변성
직접 동기화하지 않고도 안전하게 사용할 수 있는 객체는 불변하는 immutable 객체이다. 공유되는 변수에 여러스레드가 접근할때 문제가 생기는 모든 원인은 변경 가능한 변수를 사용하기 때문이다. 그런데 객체의 상태가 변하지 않는다면? 그러한 문제가 발생하지 않는다.
따라서, `불변 객체는 언제든 스레드에 안전하다`
또한, 불변 객체는 만들기도 쉽다. 불변 객체는 생성자가 적절하게 맞춘 한 가지 상태만을 유지하기 때문인데 아래 조건 만족하여 불변 객체를 만들 수 있다.
- 생성된 이후에 객체의 상태를 변경할 수 없다.
- 내부의 모든 변수는 final로 설정되어야한다.
- 적절한 방법으로 생성되어야한다. (this 변수에 대한 참조가 외부로 유출되지 않아야한다.)
불변객체라해도, 상태를 관리하기 위해 내부적으로 일반 객체나 변수를 사용할 수 있지만 불변 객체내의 다른 부분에서도 해당 값을 변경할 수 없도록 설계되어있다.
불변 객체가 그러면 어디에 쓰이는가? 싶을 것이다. 우선 `객체 불변`과 `참조 불변`을 구분해서 생각하자.
final 변수
자바의 final 키워드는 불변객체를 생성하는데 도움을 준다. 이를 적절히 활용해 초기화 안전성을 보장할 수 있기에 별다른 동기화 없이 객체를 자유롭게 사용하고 공유할 수 있다. 또한, final 변수를 지정하면 해당 변수에 어떠한 값이 들어가야하는지 고려할 범위가 줄어들기에 프로그램을 작성하는데 용이하다.
불변 객체를 공개할땐 volatile 키워드를 사용하자.
불변 객체를 통해 앞서 다뤘던 UnsafeCachingFactorizer와 AtomicReference 문제에 적용해보자.
서로 관련되어있는 여러 변수 값을 읽거나 쓸때 Race Condition이 발생할 수 있는데, 불변 객체에 해당 하는 변수를 모아두면 Race Condition을 방지할 수 있다. 아래 코드와 같이 여러값을 한꺼번에 동작시켜야한다면 여러코드를 묶는 불변 클래스를 활용하는것이 좋다.
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
안전 공개
지금까지는 객체를 특정 스레드에 한정하거나, 다른 객체 내부에 넣을때, 객체를 공개하지 않고 숨기는 방법에 대해 다루었다. 객체를 숨기기만 해서는 프로그램을 제대로 작성할 수 없고 여러 스레드에서 공유하도록 공개해야할 수 있는데 반드시 이때 안전하게 공개를 해야한다.
//안전하지 않은 객체 공개
public Holder holder;
public void initilalize(){
hodlder = new Hodler(42);
}
적절하지 않은 공개 방법: 정상적인 객체도 문제를 일으킨다.
만약,생성자가 제대로 완료되지 않았다면, 그 객체를 제대로 사용할 수 있을까? 생성자가 실행되는 도중 다른 스레드에서 인스턴스의 상태를 사용한다면 비정상적이지만 그대로 사용될 수 있고 수정한 적이 없음에도 처음과 다른 값이 생기는 경우가 존재한다.
위와 같이 안전하지 않게 클래스를 공개한 상태에서, 아래 코드와 같이 Holder 클래스가 작성되었다면, n값이 다른 AssertionError가 발생할 수 있다.
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
Holder 객체를 다른 스레드가 사용할 수 있도록 작성할때 적절한 동기화를 작성하지 않았으므로, Holder 클래스는 올바르게 공개되어있지 않다고 볼 수 있다.이렇게 올바르지 않게 공개를 했을때는 두 가지 문제가 발생할 수 있다.
첫째는, holder변수에 stale상태가 발생할 수 있다. 값을 지정한 이후에도 null 또는 이전 값이 들어가 있을 수 있다.
둘째는, 다른 스레드는 모두 holder 변수에 정상적인 참조값을 가질 수 있지만, Holder클래스는 stale 상태에 빠질 수 있다.
따라서, 적절한 동기화 방법을 적용하고 공유시키자!
불변 객체와 초기화 안전성
자바에선, 불변 객체를 공유하고자 할때 초기화 작업을 안전하게 처리할 수 있는 방법이 만들어져있다. 불변객체는 참조를 외부에 공개할때 추가적인 동기화 방법을 사용하지 않아도 스레드 안전하다. 이를 위해 불변객체는 아래의 3가지 조건을 만족해야한다.
- 상태를 변경할 수 없어야한다.
- 모든 필드의 값이 final로 선언되어야한다.
- 적절한 방법으로 생성되어야한다.
안전한 공개 방법의 특성
불변 객체가 아닌 객체는 올바르고 안전하게 공개되어야한다. 대부분은 공개하는 스레드, 사용하는 스레드 모두 양쪽에서 동기화 기법을 적용해야한다. 아래와 같은 방법을 통해 객체를 안전하게 공개할 수 있다.
- 객체에 대한 참조를 static 메소드에 초기화시킨다.
- 객체에 대한 참조를 volatile 변수, AtomicReference에 보관한다.
- 객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관한다.
- 락을 사용해 올바르게 막혀 있는 변수에 객체에 대한 참조를 보관한다.
결과적으로 불변인 객체
불변으로 선언되진 않았지만, 처음 새엇ㅇ한 이후에 그 내용이 바뀌지 않도록 만들어진 클래스에는 안전한 공개 방법을 사용하면 별다른 동기화 없이도 다른 스레드에서 얼마든지 사용할 수 있다.
(전에 배웠던 effectively final과 유사한듯)
가변 객체
객체의 생성 메서드를 실행한 후, 내용이 변경될 수 있다면 안전하게 공개했다 하더라도 공개한 상태를 다른 스레드가 볼 수 있다는 정도만 보장할 수 있다. 가변 객체를 사용할때에는 공개하는 부분과 가변 객체를 사용하는 모든 부분에 동기화 코드를 작성해야만 객체가 바뀌는 부분을 정확하게 인식할 수 있다.
다음을 유의해서 가변성에 따른 객체 공개시 필요한 점을 살피자.
- 불변 객체는 어떻게 공개해도 상관없다.
- 결과적으로 불변인 객체는 안전하게 공개해야한다.
- 가변객체는 안전하게 공개하고 스레드에 안전하게 만들거나 락으로 동기화 시켜야한다!
'PL > 모던 자바 인 액션' 카테고리의 다른 글
모던 자바 인 액션 7주차 - Chap 08 (0) | 2023.09.30 |
---|---|
모던 자바 인 액션 6주차 - Chap07 (0) | 2023.09.21 |
자바 병렬 프로그래밍 - 2장 (0) | 2023.09.07 |
자바 병렬 프로그래밍 - 1장 (0) | 2023.09.07 |
모던 자바 인 액션 5주차 - Chap06 (0) | 2023.09.07 |