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

[Java Concurrency] 객체공유

by 돼지왕 왕돼지 2017. 4. 17.
반응형


 [Java Concurrency] 객체공유


64비트, 64비트 값, atomicreference, BlockingQueue, concurrentlinkedqueue, ConcurrentMap, copyonwritearraylist, copyonwritearrayset, Double, escaped, exchanger, Final, final 변수, Future, Get, Hashtable, immutable, jvm, jvm 내부 동기화, long, mutual exclusion, new, override, pipe, Private, re-ordering, reordering, Set, stale data, static, static 메소드, synchronizedlist, synchronizedMap, synchronizedset, This, this 변수, thread safe, ThreadLocal, Vector, volatile, volatile long double, [Java Concurrency] 객체공유, 가변 객체, 가시성, 객체, 객체 공개, 객체 공유, 결과적으로 불변인 객체, 공개, 공개와 유출, 공유, 공유된 변수, 단일 스레드, 단정, 동기화, 동기화 방법 적용, 두번의 32비트 연산, 락, 로컬 변수, 리스너, 메모리 가시성, 메모리 모델, 변경 가능, 변경 가능한 객체, 변수, 불변성, 상호 배제, 생성, 설계, 성능, 숫자형, 스레드, 스레드 안전, 스레드 안전성, 스레드 한정, 스레드에 안전한 객체 공유, 스택 한정, 스테일, 스테일 데이터, 실행, 실행 순서, 안전 공개, 안정성, 에일리언 메소드, 연산의 단일성, 외부 유출, 유출 상태, 의도적 설계, 읽기 전용 객체 공유, 자바 메모리 모델, 재배치, 적절한 변수, 참조 불변, 캡슐화, 컴파일러, 클래스, 태생, 팩토리 메소드 생성자, 프로세서, 확보


3.1. 가시성

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


-
재배치(reordering) 현상을 조심해야 한다.
재배치 현상은 특정 메소드의 소스코드가 100% 코딩된 순서로 동작한다는 점을 보장할 수 없다는 점에 기인하는 문제이다.
단일 스레드로 동작할 때는 차이점을 전혀 알아챌 수 없지만 여러 스레드가 동시에 동작하는 경우에는 확연하게 나타날 수 있다.


-
동기화 기능을 지정하지 않으면 컴파일러나 프로세서, JVM 등이 프로그램 코드가 실행되는 순서를 임의로 바꿔 실행하는 이상한 경우가 발생하기도 한다.
다시 말하자면, 동기화 되지 않은 상황에서 메모리상의 변수를 대상으로 작성해 둔 코드가 '반드시 이런 순서로 동작할 것이다'라고 단정지을 수 없다.
이런 현상이 설계상의 문제점이라고 볼 수도 있지만 실제로는 JVM 이 최신 컴퓨터 하드웨어가 제공하는 기능을 100% 활용할 수 있게 의도적으로 설계한 부분이다.
따라서 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용해야 한다.


-
스테일 데이터(Stale(오래된, 신선하지 않은) Data) 는 최신값을 가지지 않은 데이터를 이야기한다.
스테일 데이터의 문제점은 항상 스테일 데이터를 사용하게 될 때도 있고, 정상적으로 동작하는 경우도 있다.
특정 스레드가 어떤 변수를 사용할 때 정상적인 최신 값을 사용할 "수"도 있고, 올바르지 않은 값을 사용할 "수"도 있다는 말이다.


-
동기화되지 않은 상태에서 특정 스레드가 변수의 값을 읽으려 한다면 스테일 상태의 값을 읽어갈 가능성이 있긴 하지만, 그래도 전혀 엉뚱한 값을 가져가는 것이 아니라 바로 이전에 다른 스레드에서 설정한 값을 가져가게 된다.
하지만 64비트를 사용하는 숫자형(double 이나 long) 에 volatile 키워드를 사용하지 않은 경우에는 난데없는 값마저 생길 가능성이 있다.


자바 메모리 모델은 메모리에서 값을 가져오고 저장하는 연산이 단일해야 한다고 정의하고 있지만,
volatile 로 지정되지 않은 long 이나 double 형의 64비트 값에 대해서는 메모리에 쓰거나 읽을 때 두번의 32비트 연산을 사용할 수 있도록 허용하고 있다.
따라서 volatile 을 지정하지 않은 long 변수의 값을 쓰는 기능과 읽는 기능이 서로 다른 스레드에서 동작한다면, 이전 값과 최신 값에서 각각 32비트를 읽어올 가능성이 생긴다.
volatile 로 지정하지도 않고 락을 사용해 동기화하지도 않은 상태로 long 이나 double 값을 동시에 여러 스레드에서 사용할 수 있다면 항상 이상한 문제를 만날 가능성이 있다.


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


-
volatile 로 선언된 변수의 값을 바꾸면 다른 스레드에서 항상 최신 값을 읽어갈 수 있다.
특정 변수를 volatile 키워드와 함께 선언하면 이 변수는 공유해 사용하기 때문에 실행 순서를 재배치(re-ordering)해서는 안 된다고 이해한다.


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


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



3.2. 공개와 유출


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


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


-
클래스에 정의는 되어 있지만 그 기능이 만들어져 있지 않은 메소드가 있을 수 있다.
이런 메소드를 에일리언 메소드라고 한다.
final 로 지정되지도 않고 private 으로 지정되지 않은 override 가능한 함수도 에릴리언 메소드에 해당된다.
어떤 객체를 이와 같이 에일리언 메소드에 인자로 넘겨주는 일도 결국에는 넘겨준 객체를 공개하는 과정이라고 생각해야 한다.


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


-
생성 메소드에서 this 변수를 유출시키는 가장 흔한 오류는 생성 메소드에서 스레드를 새로 만들어 시작시키는 일이다.
대부분의 경우 생성 메소드의 클래스와 새로운 스레드가 this 변수를 직접 공유한다
새로 만들어져 실행된 스레드에서 원래 클래스의 생성 메소드가 끝나기도 전에 원래 클래스에 정의되어 있는 여러 변수를 직접 사용할 수 있게 된다.
필요한 기능이 있어 생성 메소드에서 스레드를 생성하는 건 별 문제가 없지만, 스레드 생성과 동시에 시작시키는 건 문제의 소지가 있다.
만약 생성 메소드에서 이벤트 리스너를 등록하거나 새로운 스레드를 시작시키려면, 팩토리 메소드 생성자를 통해 진행하는 것이 좋다



3.3. 스레드 한정


-
변경 가능한 객체를 공유해 사용하는 경우에는 항상 동기화시켜야 한다.
만약 동기화를 시키지 않아야 한다면, 객체를 공유하지 말아야 한다.
어떤 객체를 단일 스레드에서만 활용한다고 확신할 수 있다면 해당 객체는 따로 동기화할 필요가 없다.


-
스택한정은 특정 객체나 변수를 로컬 변수를 통해서만 사용하게 하는 방법이다.
스택한정을 유지하기 위해서는 객체형 지역 변수가 외부에 유출되지 않도록 개발자가 직접 주의를 기울여야 한다.


-
스레드 내부의 값과 값을 갖고 있는 객체를 연결해 스레드 한정 기법을 적용할 수 있는데 그 방법은 ThreadLocal 이다.
ThreadLocal 에는 get, set 메소드가 있는데, 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리해준다.

private static ThreadLocal<Object> holder = new ThreadLocal<Object>(){

public Object initialValue(){

return sth;

}

}






3.4. 불변성


-
직접적으로 객체를 동기화하지 않고도 안전하게 사용할 수 있는 방법 중 마지막은 불변(immutable) 객체.
불변 객체는 맨 처음 생성되는 시점을 제외하고는 그 값이 전혀 바뀌지 않는 객체이다.
그래서 불변객체는 태생부터 스레드에 안전한 상태이다.


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


-
객체가 불변이라는 것과 참조가 불변이라는 것은 반드시 구분해서 생각해야 한다.


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


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



3.5. 안전 공개


-
객체를 올바르지 않게 공개하면 두 가지 문제가 발생할 수 있다.
    객체에 스테일 상태가 발생할 수 있다.
    다른 스레드에서는 정상적인 값을 가져갈 수 있지만 해당 객체 입장에서 스테일 상태에 빠질 수 있다.
    ( Object 클래스의 생성 메소드가 실행될 때 각 변수에 기본값을 채워넣게 되어 있다. 따라서 스테일 값이 생길 수 있다. )


-
final 로 선언된 변수에 변경 가능한 객체가 지정되어 있다면, 해당 변수에 들어 있는 객체의 값을 사용하려고 하는 부분을 모두 동기화 해야 한다.


-
불변 객체가 아닌 객체는 모두 올바른 방법으로 안전하게 공개해야 하며, 대부분은 공개하는 스레드와 불러다 사용하는 스레드 양쪽 모두에 동기화 방법을 적용해야 한다.


객체를 안전하게 공개하려면 해당 객체에 대한 참조와 객체 내부의 상태를 외부의 스레드가 동시에 볼 수 있어야 한다.
올바르게 생성 메소드가 실행되고 난 객체는 다음과 같은 방법으로 안전하게 공개할 수 있다.
    객체에 대한 참조를 static 메소드에서 초기화시킨다.
    객체에 대한 참조를 volatile 변수 또는 AtomicReference 클래스에 보관한다.
    객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관한다.
    락을 사용해 올바르게 막혀 있는 변수에 객체에 대한 참조를 보관한다.


-
스레드 안전한 컬렉션은 HashTable, ConcurrentMap, synchronizedMap, Vector, CopyOnWriteArrayList, CopyOnWriteArraySet, synchronziedList, synchronizedSet, BlockingQueue, ConcurrentLinkedQueue


-
Future 나 Exchanger(pipe 역할) 클래스를 적절하게 활용해도 객체를 안전하게 공개할 수 있다.


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

public static TestClass testClass = new TestClass();

static 초기화 방법은 JVM 에서 클래스를 초기화하는 시점에 작업이 모두 진행된다.
그런데 JVM 내부에서 동기화가 맞춰져 있기 때문에 이런 방법으로 객체를 초기화하면 객체를 안전하게 공개할 수 있다.


-
결과적인 불변객체는 개발 과정도 훨씬 간편하고 동기화 작업을 할 필요가 없기 때문에 프로그램의 성능을 개선하는 데도 도움이 된다.
안전하게 공개한 결과적인 불변 객체는 별다른 동기화 작업 없이도 여러 스레드에서 안전하게 호출할 수 있다.


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


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


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


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


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


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






반응형

댓글