본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java] 병렬 프로그래밍 - 객체 공유.

by 돼지왕 왕돼지 2012. 6. 29.
반응형




03. 객체 공유.


- 병렬 프로그램 작성상태가 바뀔 수 있는 내용을 프로그램 내부의 여러 부분에서 어떻게 잘 공유해 사용하도록 관리할 것인지에 대한 문제.

- 특정 블록을 단일 연산인 것처럼 동작시키거나 크리티컬 색션( critical section ) 을 구성할 때 반드시 synchronized 키워드를 사용해야 하는 건 아니다.

- 소스코드의 특정 블록을 동기화시키고자 할 떄는 항상 메모리 가시성( memory visibility ) 문제가 발생.



3.1. 가시성

- 일반적으로 특정 변수의 값을 가져갈 때 다른 스레드가 작성한 값을 가져갈 수 있다는 보장도 없고, 심지어는 값을 읽지 못할 수도 있다. 메모리상의 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.

재배치 ( reordering ) 현상은 특정 메소드의 소스코드가 100% 코딩된 순서로 동작한다는 점을 보장할 수 없다는 점에 기인하는 문제이며, 단일 스레드로 동작할 때는 차이점을 전혀 알아챌 수 없지만 여러 스레드가 동시에 동작하는 경우에는 확연하게 나타날 수 있다. 따라서 어떤 값을 영영 읽지 못해 finish가 공유되는 상황에서 while( finish ) 와 같은 구문에 봉착하면 무한 루프에 빠질 수 있다.

재배치 현상은 설계상의 문제점으로 볼 수 있지만, 실제로는 JVM이 최신 컴퓨터 하드웨어가 제공하는 기능을 100% 활용할 수 있게 "의도적"으로 설계한 부분이다. 

동기화 기능을 지정하지 않으면 컴파일러나 프로세서, JVM( 자바 가상 머신 ) 등이 프로그램 코드가 실행되는 순서를 임의로 바꿔 실행하는 이상한 경우가 발생하기도 한다. 다시 말하자면, 동기화 되지 않은 상황에서 메모리상의 변수를 대상으로 작성해둔 코드가 '반드시 이런 순서로 동작할 것이다' 라고 단정지을 수 없다.

 


3.1.1. 스테일 데이터 


스테일( stale ) 데이터는 해당 변수에 대한 최신 값이 아닌 다른 값을 사용하는 경우를 이야기한다. 특정 스레드가 어떤 변수를 사용할 때 정상적인 최신 값을 사용할 '수' 도 있고, 올바르지 않은 값을 사용할 '수'도 있다. 스테일 데이터의 위험성이 이것이다.

- 어떤 변수에서건 스테일 현상이 발생하면 예기치 못한 예외 상황이 발생하기도 하고, 데이터를 관리하는 자료 구조가 망가질 수도 있고, 계산된 결과 값이 올바르지 않을 수도 있고, 무한 반복에 빠져들 수도 있다.



3.1.2. 단일하지 않은 64비트 연산


- 동기화되지 않은 상태에서 특정 스레드가 변수의 값을 읽으려 한다면 스테일 상태의 값을 읽어갈 가능성이 있긴 하지만, 그래도 전혀 엉뚱한 값을 가져가는 것이 아니라 바로 이전에 다른 스레드에서 설정한 값을 가져가게 된다. 하지만 64비트를 사용하는 숫자형 ( double 이나 long 등) 에 volatile 키워드를 사용하지 않은 경우에는 난데없는 값마저 생길 가능성이 있다. volatile 로 지정되지 않은 long 이나 double 형의 64비트 값에 대해서는 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 사용할 수 있도록 허용하고 있다. ( 기본 자바 메모리 모델은 메모리에서 값을 가져오고 ( fetch ) 저장 ( store ) 하는 연산이 단일해야 한다고 정의한다. ) 따라서 volatile을 지정하지 않은 long 변수의 값을 쓰는 기능과 읽는 기능이 서로 다른 쓰레드에서 동작한다면, 이전 값과 최신 값에서 각각 32비트를 읽어올 가능성이 생긴다.

- volatile 로 지정하지도 않고 락을 사용해 동기화하지도 않은 상태로 long이나 double값을 동시에 여러 스레드에서 사용할 수 있다면 항상 이런 문제를 만날 가능성이 있다.



3.1.3. 락과 가시성


- 여러 스레드에서 사용하는 변수를 적당한 락으로 막아주지 않는다면, 스테일 상태에 쉽게 빠질 수 있다. 

락은 상호 배제 ( mutual exclusion ) 뿐만 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다. 변경 가능하면서 여러 스레드가 공유해 사용하는 변수를 각 스레드에서 각자 최신의 정상적인 값으로 활용하려면 동일한 락을 사용해 모두 동기화시켜야 한다.





3.1.4. volatile 변수


volatile 변수로 약간 다른 형태의 좀 더 약한 동기화 기능을 제공할 수 있다. volatile 로 선언된 변수의 값을 바꿨을 때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다. 특정 변수를 선언할 때 volatile 키워드를 지정하면, 컴파일러와 런타임 모두 '이 변수는 공유해 사용하고, 따라서 실행 순서를 재배치해서는 안 된다' 고 이해한다.

- volatile 변수를 사용할 떄에는 아무런 락이나 동기화 기능이 동작하지 않기 때문에 synchronized 를 사용한 동기화보다는 아무래도 강도가 약할 수 밖에 없다.

- volatile 변수를 읽는 연산은 volatile 이 아닌 변수를 읽는 시간보다 아주 약간 더 느리다.

- 메모리 가시성의 입장에서 본다면 volatile 변수를 사용하는 것과 synchronized 키워드로 특정 코드를 묶는 게 비슷한 효과를 가져오고, volatile 변수의 값을 읽고 나면 synchronized 블록에 진입하는 것과 비슷한 상태에 해당한다. 

- volatile 변수에 너무 의존하지 않는 게 좋다. volatile 변수만 사용해 메모리 가시성을 확보하도록 작성한 코드는 synchronized 로 직접 동기화한 코드보다 훨씬 읽기가 어렵고, 따라서 오류가 발생할 가능성도 높다.

동기화하고자 하는 부분을 명확하게 볼 수 있고, 구현하기가 훨씬 간단한 경우에만 volatile 변수를 활용하자. 반대로 작은 부분이라도 가시성을 추론해봐야 하는 경우에는 volatile 변수를 사용하지 않는 것이 좋다. volatile 변수를 사용하는 적절한 경우는, 일반적으로 변수에 보관된 클래스의 상태에 대한 가시성을 확보하거나 중요한 이벤트 ( 초기화, 종료 등 ) 가 발생했다는 등의 정보를 정확하게 전달하고자 하는 경우 등이 해당된다.


- volatile 변수는 굉장히 간편하게 사용할 수 있는 반면 제약 사항도 있다.

 - 단 하나의 스레드에서만 사용한다는 보장이 없는 상태라면, volatile 연산자의 기본적은 능력으로는 증가 연산자 ( count++ ) 를 사용한 부분까지 동기화를 맞춰주지는 않는다. ( 단인 연산 변수를 사용하면 읽고, 변경하고, 쓰는 부분에 모두 단일 연산을 보장하기 때문에 '좀 더 나온 volatile 변수'로 사용할 수 있다.

 

락을 사용하면 가시성과 연산의 단일성을 모두 보장받을 수 있다. 하지만 volatile 변수는 연산의 단일성은 보장하지 못하고, 가시성만 보장한다.


- volatile 변수는 다음과 같은 상황에서만 사용하는 것이 좋다. 
 * 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나 해당 변수의 값을 변경하는 스레드가 하나만 존재
 * 해당 변수가 객체의 불변조건을 이루는 다른 변수와 달리 불변조건에 관련되어 있지 않다.
 * 해당 변수를 사용하는 동안에는 어떤 경우라도 락을 걸어 둘 필요가 없는 경우.

 




3.2. 공개와 유출

- 특정 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만들면 공개( published ) 되었다고 한다. 스코프 밖의 코드에서 볼 수 있는 변수에 스코프 내부의 객체에 대한 참조를 저장하거나, private 이 아닌 메소드에서 호출한 메소드가 내부에서 생성한 객체를 리턴하거나, 다른 클래스의 메소드로 객체를 넘겨주는 경우 등이 해당된다. 

- 특정 객체를 공개해서 여러 부분에서 공유해 사용할 수 있도록 만들기도 하는데, 이런 경우에는 반드시 해당 객체를 동기화시켜야 한다. 만약 클래스 내부의 상태 변수를 외부에 공개해야 한다면 객체 캡슐화 작업이 물거품이 되거나 내부 데이터의 안정성을 해칠 수 있다.

- 의도적으로 공개시키지 않았지만 외부에서 사용할 수 있게 공개된 경우를 유출 상태 ( escaped ) 라고 한다.

- 특정 객체 하나를 공개한다고 해도, 그와 관련된 다른 객체까지 덩달아 공개하게 되는 경우도 많다. 이는 Collection 을 사용할 때 주로 발생한다. 객체를 공개하면 private 이 아닌 모든 변수 속성에 연결되어 있는 모든 객체가 함께 공개된다. 객체를 공개했을 때 그 객체 내부의 private 이 아닌 변수나 메소드를 통해 불러올 수 있는 모든 객체는 함께 공개된다.

- 정의는 되어 있지만 그 기능이 만들어져 있지 않은 메소드들을 에일리언 메소드라고 하는데, 보통 오버라이드 용으로 사용된다. final 로 지정되지도 않고, private 으로 지정되지도 않은 이 에일리언 메소드에 인자로 넘겨주는 일도 결국에는 넘겨준 객체를 공개하는 과정이라고 생각해야 한다.

- 다른 스레드에 공개된 객체를 사용해 실제로 어떤 작업을 하지 않기 때문에 별문제가 아니라고 생각할 수도 있겠지만, 항상 누군가는 의도했건 의도하지 않았건 공개된 객체를 잘못 사용할 가능성에 노출되는 셈이다.



3.2.1. 생성 메소드 안전성


- 일반적으로 생성 메소드가 완전히 종료하고 난 이후가 되어야 객체의 상태가 개발자가 예상한 상태로 초기화되기 때문에 생성 메소드가 실행되는 도중에 해당 객체를 외부에 공개한다면 정상적이지 않은 상태의 객체를 외부에서 불러 사용할 가능성이 있다. 이런 일은 공개하는 코드가 생성 메소드의 가운데 부분에 있을 뿐만 아니라, 생성 메소드의 가장 마지막 부분에 공개하는 코드가 있다 해도 충분히 가능한 일이다. 생성 메소드 실행 도중에 this 변수가 외부에 공개된다면, 이론적으로 해당 객체는 정상적으로 생성되지 않았다고 말할 수 있다.

생성 메소드를 실행하는 도중에는 this 변수가 외부에 유출되지 않게 해야 한다.



- 생성 메소드에서 this 변수를 유출시키는 가장 흔한 오류는 생성 메소드에서 스레드를 새로 만들어 시작시키는 일이다. 생성 메소드에서 또 다른 스레드를 만들어 내면 대부분의 경우에는 생성 메소드의 클래스와 새로운 스레드가 this 변수를 직접 공유( 스레드를 생성할 때 인자로 넘겨줌) 하거나 자동으로 공유되기도 한다.

- 필요한 기능이 있다면 생성 메소드에서 스레드를 '생성' 하는건 별 문제가 없는 일이지만, 스레드를 생성과 동시에 시작 시키는 건 문제의 소지가 많은 일이다. 스레드를 시작시키는 기능을 start 나 initialize 등의 메소드로 만들어 사용하는 편이 좋다.

- 생성 메소드에서 오버라이드 가능한 다른 메소드( private 도 아니고 final 도 아닌 )를 호출하는 경우가 있다면 this 참조가 외부에 유출된 가능성이 있다.

- 새로 작성하는 클래스의 생성 메소드에서 이벤트 리스너를 등록하거나 새로운 스레드를 시작시키려면, 생성 메소드를 private 으로 만들고 public 으로 지정된 팩토리 메소드를 만들어 사용하는 방법이 좋다.





3.3. 스레드 한정

- 객체 인스턴스를 특정 스레드에 한정시켜두면, 해당하는 객체가 자동으로 스레드 안전성을 확보하게 된다. ( ex: swing )

- 임의의 객체를 특정 스레드에 한정시키는 기능은 제공되지 않는다. 스레드 한정 기법은 프로그램을 처음 설계하는 과정부터 함께 다뤄야 하며, 프로그램을 구현하는 과정 내내 한정 기법을 계속해서 적용해야 한다.


3.3.1. 스레드 한정 - 주먹구구식




3.3.2. 스택 한정


- 스택 한정 기법은 특정 객체를 로컬 변수를 통해서만 사용할 수 있는 특별한 경우의 스레드 한정 기법이라고 할 수 있다.

- 기본 변수형을 사용하는 로컬 변수는 언어적으로 스택 한정 상태가 보장된다.

- 객체형 변수가 스택 한정 상태를 유지할 수 있게 하려면 해당 객체에 대한 참조가 외부로 유출되지 않도록 개발자가 직접 주의를 기울여야 한다.

- 해당 객체를 현재 스레드에 한정해야 한다는 요구사항과 해당 객체가 스레드에 안전하지 않다는 점은 대부분 코드를 처음 작섷했던 개발자만 인식할 뿐, 후임 개발자는 전달받지 못하는 경우가 많다. 따라서 이런 점을 명확하게 정리해 누구든지 알아볼 수 있도록 표시해 두는 것이 좋다. 그렇지 않으면 후임자가 바귈 때마다 코드를 수정하면서 해당 객체를 외부에 공개할 가능성도 높아진다.



3.3.3. ThreadLocal


- 스레드 내부의 값과 값을 갖고 있는 객체를 연결해 스레드 한정 기법을 적용할 수 있도록 도와주는 좀 더 형식적인 방법으로 ThreadLocal 이 있다. ThreadLocal 클래스에는 get 과 set 메소드가 있는데 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리해준다. 다시 말해 ThreadLocal 클래스의 get 메소드를 호출하면 현재 실행 중인 스레드에서 최근에 set 메소드를 호출해 저장했던 값을 가져올 수 있다.

- 특정 스레드가 ThreadLocal.get메소드를 처음 호출한다면 initialValue 메소드에서 값을 만들어 해당 스레드에게 초기 값으로 넘겨준다. 개념적으로 본다면 ThreadLocal<T> 클래스는 Map<Thread, T> 라는 자료 구조로 구성되어 있다.

- 원래 단일 스레드에서 동ㅈ가하던 기능을 멀티스레드 환경으로 구성해야 할 때, 그 의미에 따라 다르지만 공유된 전역 변수를 ThreadLocal을 활용하도록 변경하면 스레드 안전성을 보장할 수 있다.





3.4. 불변성

- 직접적으로 객체를 동기화하지 않고도 안전하게 사용할 수 있는 방법 가운데 마지막으로 알아볼 내용은 바로 불변 immutable 객체이다. 

- 불변 객체는 맨 처음 생성되는 시점을 제외하고는 그 값이 전혀 바뀌지 않는 객체를 말한다. 따라서 불변 객체는 태생부터 스레드에 안전한 상태이다.

불변 객체는 언제라도 스레드에 안전하다.

 
- 객체 내부의 모든 변수를 final 로 설정한다 해서 해당 객체가 불변이지는 않다. 변수에 참조로 연결되어 있는 객체가 불변 객체가 아니라면 내용이 바뀔 수 있기 때문이다.

다음 조건을 만족하면 해당 객체는 불변 객체다.
1. 생성되고 난 이후에는 객체의 상태를 변경할 수 없다.
2. 내부의 모든 변수는 final 로 설정돼야 한다.
3. 적절한 방법으로 생성돼야 한다. ( 예를 들어 this 변수에 대한 참조가 외부로 유출되지 않아야 한다. ) 

 
- '객체'가 불변이라는 것과 '참조'가 불변이라는 것은 반드시 구분해서 생각해야 한다. 프로그램이 사용하는 데이터가 불변 객체에 들어있다 해도, 해당 객체를 가리키고 있는 참조 변수에 또 다른 불변 객체를 바꿔치기하면 프로그램의 데이터가 언제든지 바귀는 셈이다.



3.4.1. final 변수


- final 을 지정한 변수의 값은 변경할 수 없다. ( 물론 벼눗가 가리키는 객체가 불변 객체가 아니라면 해당 객체에 들어 있는 값은 변경할 수 있다. )

외부에서 반드시 사용할 일이 없는 변수는 private 로 선언하는 게 괜찮은 방법인 만큼, 나중에 변경할 일이 없다고 판단되는 변수는 final 로 선언해두는 것도 좋은 방법이다.





3.4.2. 예제 : 불변 객체를 공개할 때 volatile 키워드 사용


- 만약 여러 개의 값이 단일하게 한꺼번에 행동해야 한다면, 여러 개의 값을 한데 묶는 불변 클래스를 만들어 사용하는 방법이 좋다. ( 이것의 사용시는 volatile 키워드 사용 )

- 서로 관련되어 있는 여러 개의 변수 값을 서로 읽거나 쓰는 과정에 경쟁 조건이 발생할 수 있는데, 불변 객체에 해당하는 변수를 모두 모아두면 경쟁 조건을 방지할 수 있다. 여러 개의 변수를 묶어 사용하고자 할 때, 불변 객체가 아닌 일반 객체를 만들어 사용하면 락을 사용해야 연산의 단일성을 보장할 수 있다. 하지만 불변 객체에 변수를 묶어두면 특정 스레드가 불변 객체를 사용할 때 다른 스레드가 불변 객체 값을 변경하지 않을까 걱정하지 않아도 된다. 







3.5. 안전 공개

- 객체에 대한 참조를 public 변수에 넣어 공개하는 것은 객체를 공개하는 그다지 안전한 방법이 아니다. 



3.5.1. 적절하지 않은 공개 방법 : 정상적인 객체도 문제를 일으킨다.

 
- 생성 메소드가 실행되고 있는 상태의 인스턴스를 다른 스레드가 사용하려 한다면 비정상적인 상태임에도 불구하고 그대로 사용하게 될 가능성이 있고, 나중에 생성 메소드가 제대로 끝나고 보니 공개한 이후에 값이 바뀐 적이 없음에도 불구하고 처음 사용할 때와는 값이 다른 경우도 생긴다. 

-  자바에서 모든 클래스는 Object 클래스를 상속받도록 되어 있는데, 상속받은 클래스의 생성 메소드가 실행되기 전에 Object 클래스의 생성 메소드가 실행되어 각 변수에 기본값을 채워넣게 되어 있다. 따라서 Object 클래스가 기본값으로 지정한 내용을 스테일 값으로 가져올 가능성이 있다.



3.5.2. 불변 객체와 초기화 안전성

 
- 자바 메모리 모델에는 불변 객체를 공유하고자 할 때 초기화 작업을 안전하게 처리할 수 있는 방법이 만들어져 있다. 외부 스레드에서 항상 정상적인 값을 참조하려면 동기화 방법이 필요하다. 하지만 불변 객체를 사용하면 객체의 참조를 외부에 공개할 때 추가적인 동기화 방법을 사용하지 않았다 해도 항상 안전하게 올바른 참조 값을 사용할 수 있다.

불변 객체는 별다른 동기화 방법을 적용하지 않았다 해도 어느 스레드에서건 마음껏 안전하게 사용할 수 있다. 불변 객체를 공개하는 부분에 동기화 처리를 하지 않았다 해도 아무런 문제가 없다.

 



3.5.3. 안전한 공개 방법의 특성


객체를 안전하게 공급하려면 해당 객체에 대한 참조와 객체 내부의 상태를 외부의 스레드가 동시에 볼 수 있게 해야 한다.  올바르게 생성 메소드가 실행되고 난 객체는 다음과 같은 방법으로 안전하게 공개할 수 있다.

1. 객체에 대한 참조를 static 메소드에서 초기화한다.
2. 객체에 대한 참조를 volatile 변수 또는 AtomicReference 클래스에 보관한다.
3. 객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관한다.
4. 락을 사용해 올바르게 막혀 있는 변수에 객체에 대한 참조를 보관한다. 


- 자바에서 지원하는 스레드 동기화 class 들.
 * Hashtable, ConcurrentMap, synchronizedMap
 * Vector, CopyOnWriteArrayList, CopyOnWriteArraySet
 *  BlockingQueue, ConcurrentLinkedQueue

-  static 변수를 선언할 때, 직접 new 연산자로 생성 메소드를 실행해 개체를 생성할 수 있다면 가장 쉬우면서도 안전한 객체 공개 방법.




3.5.4. 결과적으로 불변인 객체


안전하게 공개한 결과적인 불변 객체는 별다른 동기화 작업 없이도 여러 스레드에서 안전하게 호출해 사용할 수 있다.





3.5.5. 가변 객체


- 가변 객체를 안전하게 사용하려면 안전하게 공개해야만 하고, 또한 동기화와 락을 사용해 스레드 안전성을 확보해야만 한다.

가변성에 따라 객체를 공개할 때 필요한 점을 살펴보면 다음과 같다.
- 불변 객체는 어떤 방법으로 공개해도 아무 문제가 없다.
- 결과적으로 불변인 객체는 안전하게 공개해야 한다.
- 가변 객체는 안전하게 공개해야 하고, 스레드에 안전하게 만들거나 락으로 동기화시켜야 한다. 

 
 


3.5.6. 객체를 안전하게 공유하기.


여러 스레드를 동시에 사용하는 병렬 프로그램에서 객체를 공유해 사용하고자 할 떄 가장 많이 사용되는 몇 가지 원칙을 살펴보면 다음과 같다.

1. 스레드 한정 : 스레드에 한정된 객체는 안전하게 해당 스레드 내부에 존재하면서 그 스레드에서만 호출해 사용할 수 있다.

2. 읽기 전용 객체를 공유 : 읽기 전용 객체를 공유해 사용한다면 동기화 작업을 하지 않더라도 여러 스레드에서 언제든지 마음껏 값을 읽어 사용할 수 있다. 물론 읽기 전용이기 때문에 값이 변경될 수는 없다. 불변 객체와 결과적으로 불변인 객체가 읽기 전용 객체에 해당한다고 볼 수 있다.

3. 스레드에 안전한 객체를 공유 : 스레드에 안전한 객체는 객체 내부적으로 필수적인 동기화 기능이 만들어져 있기 때문에 외부에서 동기화를 신경 쓸 필요가 없고, 여러 스레드에서 마음껏 호출해 사용할 수 있다.

4. 동기화 방법 적용 : 특정 객체에 동기화 방법을 적용해두면 지정한 락을 획득하기 전에는 해당 객체를 사용할 수 없다. 스레드에 안전한 객체 내부에서 사용하는 객체나 공개된 객체 가운데 특정 락을 확보해야 사용할 수 있도록 막혀 있는 객체 등에 동기화 방법이 적용되어 있다고 볼 수 있다. 




도움이 되셨다면 손가락 꾸욱~




 




반응형

댓글