자바 병렬 프로그래밍 - 객체 공유
병렬 프로그램 작성은 상태가 바뀔 수 있는 내용을 어떻게 잘 공유해 사용하도록 관리하는지에 대한 문제라 했다.
2장에서는 멀티 스레드가 동시에 동일한 데이터에 접근하는 것을 막기 위해
synchronization 를 사용하는 방법을 설명했다.
이번 장에서는 여러 개의 스레드에서 특정 객체를 동시에 사용하려 할때 안전하게 동작 하도록
객체를 공유하고 공개하는 방법을 알아보자
Synchronization 에는 메모리 가시성(memory visibillity) 라는 또 다른 중요한 측면이 있다.
특정 변수의 값을 사용하고 있을 때, 다른 스레드가 해당 변수의 값을 접근하지 못하게 막는것 뿐만 아니라,
값이 동기화 블록을 빠져나가고 나면, 다른 스레드가 변경된 값을 바로 쓸 수 있어야한다는 것이다.
적절한 방법으로 동기화 시키지 못하면 다른 스레드에서 값을 제대로 사용하지 못하는 경우가 생길 수 있다.
따라서 항상 객체를 명시적으로 동기화 시키거나 객체 내부에 적절한 동기화 기능을 내장시켜야 한다.
라이브러리 클래스에 내장된 synchronization를 이용하거나 명시적 synchronization를 써서 객체를 안전하게 published 할 수 있다.
가시성 Visible
일단 메모리 가시성이 뭐냐하면
한 스레드에서 변경한 특정 메모리 값이 다른 스레드에서 제대로 읽어지는가를 가시성이라 한다.
보통 가시성을 보장하기 위해 뮤텍스나 임계영역(critical section) 을 사용해 메모리 장벽을 만든다고 한다.
NoVisitillity 클래스에서는 Synchronization 하지 않고 데이터를 공유 할 때 문제가 발생할 수 있는 상황을 보여준다.
main 스레드와 reader 스레드가 있다.
ready 와 number 공유 변수에 접근을 한다.
메인 스레드는 reader 스레드를 시작하고 number를 42로 바꾸고 ready 를 true 로 바꾼다.
리더 스레드는 ready 가 true 가 됐을때 number를 print 하는데
42가 print 되어야 할것 같지만 그렇지 않다.
0을 print 할 가능성이 높고 terminate 도 되지 않는다.
적절한 동기화가 없기 때문에 메인 스레드가 쓴 값들이
reader 스레드에 보일 것이라는 보장이 없다.
웃기게도 ready 가 true 가 되면서 0이 print 될 수 도있고
ready 가 안적혀서 영원히 안 끝날수도있다.
이걸 재배치(reordering) 이라 한다.
특정 메소드의 소스코드가 100% 코딩된 순서로 보장할 수 없다는 점
멀티 스레드 환경에서는 synchronization 을 사용해야 메모리 visibillity 를 보장 할 수 있다
- stale data
- 진부한 데이터 (이전 데이터)
위 예제로 동기화 하지 않으면 최신 값이 아닐 수 있는 놀라운 결과를 초래할 수 있다는 걸 보여줬다.
더 큰 문제는 스테일 데이터가 나올 수도 있고, 정상적으로 동작 할 때도 있다는 것이다
3.2 는 스레드 세이프 하지않다. value 필드에 동기화 되지 않고 get 과 set 모두에서 접근 되기 때문에
한 스레드가 set 한 후 다른 스레드가 get을 호출 하는 경우 해당 업데이트를 표시하지 않을 수 있다.
우리는 synchronzied 를 이용해 스레드 세이프 하게 만들수있다.
setter 만 Synchronizing 하는건 충분하지 않다.
스레드를 호출할때 여전히 stale value 를 볼 수 있기 때문
- 64비트 명령의 non 원자성
64비트의 명령어인 long 이나 double 을 사용할때,
자바 메모리 모델은 fetch 그리고 명령어를 저장할때 원자(atomic) 적이지만,
nonvolatile 인 long 과 double 을 사용할때는
JVM 이 64 비트를 읽거나 쓸 때 32 비트의 명령어 2개로 나눠서 사용하는걸 허용한다.
그래서 위 32비트 는 다른값이 나오고 밑 32 비트는 다른 값이 나올 수 있다.
그래서 64비트의 명령어를 사용할땐 volatile 이나 lock 으로 막아주는게 중요하다
- 락과 가시성 locking and visibillity
스레드 A 가 동기화 블록을 실행 한 후 스레드 B 가 같은 lock 으로 보호 되고 있는
동기화 블록으로 들어오게 되면
잠금을 해제하기 전에 A 가 볼 수 있었던 변수 값은 B가 잠금을 획들할때 볼 수 있도록 보장된다.
즉, 동기화된 블록 내에서 A가 한 모든 작업은
동일한 lock 으로 보호되는 B가 실행할 때 안전하게 사용할 수 있다는 말이다.
물론 synchronization 되었을때만,
- Volatile variable
java 는 약한 형태의 synchronization 을 제공한다.
바로 volatile variables 이다.
volatile 로 선언된 변수의 값을 바꿨을때 다른 스레드에서 항상 최신 값을 읽어 갈 수 있도록 해준다.
voliatile 변수는 레지스터에 캐시 되지 않고 프로세서 외부 캐시에도 들어가지 않기 때문에
변수의 값을 읽으면 항상 최신 값을 읽을 수 있다.
필드 값이 volatile 로 선언이 되게 되면, 컴파일러와 런타임시에 이 변수는 다른 메모리 operations 들과
reorder 되면 안된다고 알려준다.
volatile 을 쓰는 경우
- 변수의 값을 변경하는 스레드가 하나만 존재
- 다른 변수와 달리 불변조건에 관련이 없을때
- 변수에 접근 할때 락을 걸 필요가 없는경우
공개와 유출 Publication and Escape
객체를 현재 코드 스코프 외에서도 사용 할 수 있게 하는걸 공개되었다고 한다.
특정 객체를 공유해서 사용 할 때는 반드시 객체를 동기화 시켜야 한다.
public static 변수에 객체를 설정하면 직접적으로 해당 객체를 모든 클래스와 스레드에서 사용할수있게 공개한 셈이다.
private 으로 지정된 배열 값을 public 메소드로 공개가 됐다.
getStates 를 호출하는 클라이언트 쪽에서 states 를 직접 변경할수있게 되어서 권장하지 않는 방식이다.
private 변수가 public 메소드를 통해 유출 상태에 놓여 있다고 할 수 있다.
다른 스레드가 유출된 클래스를 의도적이건 의도적이지 않건 잘 못 사용할 가능성이 생긴다.
객체가 유출되는 상황에서 문제점을 겪을 수 있기 때문에 객체 내부는 캡슐화 해야 한다.
적절하게 캡슐화가 되어있다면, 프로그램이 정상적으로 동작할것이라고 예측하기 쉬워진다.
- 생성 메소드 안전성
클래스의 생성자 메소드에서 이벤트 리스너를 등록하거나, 새로운 스레드를 시작시키려면,
팩토리 메소드를 만들어 사용하는게 좋다.
스레드 한정
변경 가능한 객체를 공유해 사용할 때는 항상 동기화 해야 한다.
특정 객체가 단일 스레드에서만 사용되는게 확신 되면
해당 객체는 따로 동기화 할 필요가 없다.
이렇게 스레드를 한정 하는 방법으로 스레드 안전성을 확보 할 수 있다.
스레드 한정 기법을 사용하는 예로는
JDBC 가 Connection 객체를 풀링해 사용하는 방법이 있다.
DB 풀은 한쪽에서 DB 연결을 사용하는 동안 다른 스레드가 사용하지 못하게 막기 때문에,
공유하는 Connection 객체를 풀로 관리하면 특정 Connection 을 하나 이상의 스레드가 사용하지 못하도록 한정 할 수 있다.
-스레드 한정 - 주먹구구식
임시방편으로 스레드 한정 기법을 쓸 수 있다
특정 모듈의 기능을 단일 스레드로 동작하도록 구현하면, 언어적인 지원 없이 만든 스레드 한정 기법에서 나타 날 수 있는 오류를 최소화 할 수 있다.
volatile 변수는 쓰기 작업은 특정 스레드 한 곳에서만 쓸 수 있게 제한 해야 정확한 값을 읽어 갈 수 있다.
임시 방편이라 완벽하게 안전성을 보장 하지 않는다.
가능하면 스택 한정 이나 스레드 로컬 방식을 사용하자
- 스택 한정
스택 한정 기법은 특정 객체를 로컬 변수를 통해서만 사용할 수 있는 특별한 한정 기법이라고 할 수 있다.
로컬 변수는 현재 실행중인 스레드에 한정 되어 있다고 볼 수 있다.
스레드 내부의 스택에서만 존재하기 때문에 다른 스레드에서 볼 수 가 없다.
주의 해야 할 점은 로컬 변수가 외부로 유출되어서 참조를 다른 스레드에서 사용 할 수 없도록 해야 한다는 점이다.
- ThreadLocal
ThreadLocal 클래스에는 get 과 set 메소드가 있는데 호출 하는 스레드마다 다른 값을 사용할 수 있도록
관리 해준다.
즉, get 메소드를 호출하면 현재 실행 중인 스레드에서 최근데 set 을 호출해 변경했던 값을 가져와 쓸 수 있다는 것
예를들어 db 에 매번 Connection 인스턴스를 만들어서 접속 하는게 부담스러워서
Connection 인스턴스를 하나 만들어 전역 변수로 쓴다고 생각해보자
근데 JDBC 연결은 스레드 세이프 하지 않기 때문에 멀티스레드 어플리케이션에서
적절한 동기화 없이 전역 변수로 사용하면 스레드 세이프 하지 않게 된다.
이때 스레드 로컬을 사용하면 스레드는 저마다 다른 연결 객체를 갖을 수 있다.
편리하지만, 전역 변수 처럼 동작하기 때문에 전역 변수를 남발하는 결과를 초래 할 수 도 있다.
불변성
객체를 동기화 하지 않고도 안전하게 사용할 수 있는 방법 중 마지막으로 볼게
불변 객체이다.
거의 모든 문제가 여러개의 스레드가 예측할 수 없게 변경 가능한 값을 동시에 사용하기 때문이다.
근데 만약 객체가 상태가 변하지 않는다면?
모든 문제가 사라진다.
불변 객체는 맨 처음 생성되는 시점을 제외하고는 그 값이 전혀 바뀌지 않는 객체를 말한다.
따라서 불변 객체는 언제나 스레드에 안전하다.
불변 객체는 만들기 쉽고, 안전하다
불변 객체를 만들기 위한 조건
- 생성되고 난 이후에는 객체의 상태를 변경 할 수 없다
- 내부의 모든 변수는 final 로 설정해야 한다 (아니여도 만들 수 있지만 조금 더 전문적인 지식이 필요, String 참고)
- 적잘한 방법으로 생성되야 한다. (생성자에 this 변수가 외부로 유출되면 안된다)
생성 메소드 이후에 set 변수를 변경 할 수 없다.
this 변수에 대한 외부 유출도 없다.
- final 변수
final 키워드는 불변 객체를 생성할때도 도움을 준다.
final 키워드 변수의 값은 변경 할 수 없는데,
자바 메모리 모델을 보면 약간 특별한 의미를 찾을 수 있다.
final 키워드를 사용하면 별다른 동기화 작업 없이도 불변 객체를 사용하고 공유할 수 있다.
* 나중에 변경 할 일 이 없다고 판단되면 final 로 선언 해두면 좋다
- 불변 객체를 공개할 때 volatile 키워드 사용
불변 객체에 volatile 키워드를 쓰면 스레드에 안전하다.
안전 공개
지금 까지는 객체를 특정 스레드에 한정하거나,
객체 내부에 넣을 때 객체를 공개 하지 않고 확실하게 숨기는 방법을 봤다.
하지만 여러 스레드에 공유하도록 공개 해야 할 상황이 있을 수 있다.
이럴때는 반드시 안전하게 사용해야 한다.
생성 메소드가 채 끝나기 전에 다른 스레드가 객체를 사용 할 수 있다.
- 적절하지 않은 공개 방법 : 정상적인 객체도 문제를 일으킨다
다른 스레드가 사용 할 수 있는데 적절한 동기화 방법이 적용되지 않았다.
- holder 변수에 값을 지정한 이후에도 null 값이나 이전 참조 값이 들어있을 수 있다. (stale 상태)
여러개의 스레드에서 사용하도록 공유할때는 적절한 동기화 방법이 필요하다
- 불변 객체와 초기화 안전성
자바 메모리 모델에는 불변 객체를 공유 할때 초기화 작업을 안전하게 처리 하는 방법이 만들어져 있다.
안전하게 초기화 하려면 다음 불변 객체 요구조건을 만족해야한다.
- 상태를 변경 할 수 없어야 한다
- 모든 필드의 값이 final 이어야 한다.
- 적절한 방법으로 생성해야 한다.
- 안전한 공개 방법의 특성
불변 객체가 아닌 객체는 모두 안전하게 공개해야 하며, 대부분은
공개하는 스레드와 사용하는 스레드 양쪽 모두 동기화 방법을 적용해야 한다.
올바르게 생성 메소드가 실행되고 난 객체는 다음과 같은 방법으로 안전하게 공개 할 수 있다.
- 객체에 대한 참조를 static 메소드에서 초기화 시킨다.
- 객체에 대한 참조를 volatile 변수 또는 AtomicReference 클래스에 보관한다.
- 객체에 대한 참조를 올바르게 생성된 클래스의 내부 final 변수에 보관한다.
- 락을 사용해 막혀있는 변수에 객체에 대한 참조를 보관한다.
- 결과적으로 불변인 객체
처음 생성한 이후 그 내용이 바뀌지 않도록 만들어진 클래스는 안전하다.
기술적으로는 불변이 아닐 수 있지만, 그 내용이 변경 되지 않는다고 하면 결과론적으로는
불변 객체라고 볼 수 있다.
- 가변 객체
가변 객체를 사용할 때에는 공개하는 부분과 사용하는 부분 모두 동기화 코드를 작성해야 한다.
동기화와 락을 통해 스레드 안전성을 확보 해야 한다.
- 객체를 안전하게 공유하기
객체를 공유해 사용하고자 할 때 가장 많이 사용되는 몇가지 원칙을 보면
- 스레드 한정 : 스레드 내부에 존재하면서 그 스레드에서만 호출해 사용 할 수 있다.
- 읽기 전용 객체를 공유 : 읽기 전용 객체를 공유해 사용하면 동기화 하지 않아도 여러 스레드에서 마음껏 사용 할 수 있다. 값이 변경 되지 않기 때문
- 스레드에 안전한 객체를 공유: 안전한 객체는 동기화 기능이 만들어져 있기 때문에 외부에서 신경 쓰지 않고 사용해도 된다.
- 동기화 방법 적용: 특정 객체에 동기화 방법을 적용해두면 지정한 락을 획득하기 전에는 해당 객체를 사용할수없다.