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

[Java Concurrency] 스레드 안전성

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


 [Java Concurrency] 스레드 안전성


@guardedby, annotation, atomiclong, class 객체, guardedby, intrinsic lock, monitor lock, mutex, mutual exclusion lock, posix 스레드, pthread, reentrant, static synchronized, synchronized, [Java Concurrency] 스레드 안전성, 객체 지향 기법, 경쟁 조건, 계산 작업, 공유된 상태에 대한 접근, 네트웍 작업, 단순성, 대기 상태, 데이터 은닉, 동기화, 동기화 원칙, 락, 락으로 상태 보호하기, 메소드 내부 코드, 모니터 락, 뮤텍스, 병렬 처리 오류, 부하, 불변 객체, 불변 조건, 사용자 입출력 작업, 상충, 상태, 상태 변수, 상태 없는 객체, 상태의 접근, 상호 배제 락, 설계, 성능, 스레드 간 공유, 스레드 단위, 스레드 안전, 스레드만, 스케줄, 스케쥴, 암묵적인 락, 여러 변수에 대한 불변조건, 오래 걸리는 작업, 왕도, 유혹, 읽기 쓰기, 재진입, 접근, 캡슐화, 클래스 인스턴스 락, 타이밍, 프로그램 규모, 활동성, 활동성과 성능, 희생


-
스레드에 안전한 코드를 작성하는 것은 근본적으로는 상태, 특히 공유되고 변경할 수 있는 상태에 대한 접근을 관리하는 것이다.


-
공유된 상태에 대한 접근을 동기화해야 한다는 원칙에 "특별한" 경우의 예외가 있다고 생각하고 싶겠지만, 그런 유혹은 버려야 한다.


-
만약 여러 스레드가 변경할 수 있는 하나의 상태 변수를 적절한 동기화 없이 접근하면 그 프로그램은 잘못된 것이다. 이렇게 잘못된 프로그램을 고치는 데는 세 가지 방법이 있다.

1. 해당 상태 변수를 스레드 간에 공유하지 않거나
2. 해당 상태 변수를 변경할 수 없도록 만들거나
3. 해당 상태 변수에 접근할 땐 언제나 동기화를 사용한다.


-
스레드 안전성을 확보하기 위해 나중에 클래스를 고치는 것보다는 애당초 스레드에 안전하게 설계하는 편이 훨씬 쉽다.
프로그램의 규모가 커지면 특정 변수를 여러 스레드에서 접근하는지 파악하는 일조차 간단치 않을 수 있다.


-
캡슐화나 데이터 은닉같은 기법이 스레드에 안전한 클래스를 작성하는 데도 도움이 될 수 있다.
스레드 안전한 클래스를 설계할 땐, 바람직한 객체 지향 기법이 왕도다.
캡슐화와 불변 객체를 잘 활용하고, 불변 조건을 명확하게 기술해야 한다.



2.1. 스레드 안정성이란?


-
여러 스레드가 클래스에 접근할 때 계속 정확하게 동작하면 해당 클래스는 스레드 안전하다.


-
여러 스레드가 클래스에 접근할 때, 실행 환경이 해당 스레드들의 실행을 어떻게 스케줄하든 어디에 끼워 넣든, 호출하는 쪽에서 추가적인 동기화나 다른 조율 없이도 정확하게 동작하면 해당 클래스는 스레드 안전하다고 말한다.


-
스레드 안전한 클래스는 클라이언트 쪽에서 별도로 동기화할 필요가 없도록 동기화 기능도 캡슐화한다


-
상태 없는 객체는 항상 스레드 안전하다.



2.2. 단일 연산


-
대부분의 병렬 처리 오류가 경쟁 조건 때문에 프로그램에 오류가 항상 발생하지 않으며,
운 나쁘게 타이밍이 꼬일 때만 문제가 발생한다.


-
작업 A를 실행 중인 스레드 관점에서 다른 스레드가 작업 B를 실행할 때 작업 B가 모두 수행됐거나 또는 전혀 수행되지 않은 두 가지 상태로만 파악된다면 작업 A의 눈으로 볼 때 작업 B는 단일 연산이다.
단일 연산 작업은 자신을 포함해 같은 상태를 다루는 모든 작업이 단일 연산인 작업을 지칭한다.


-
가능하면 클래스 상태를 관리하기 위해 AtomicLong 처럼 스레드에 안전하게 이미 만들어져 있는 객체를 사용하는 편이 좋다.
스레드 안전하지 않은 상태 변수를 선언해두고 사용하는 것보다 이미 스레드 안전하게 만들어진 클래스가 가질 수 있는 가능한 상태의 변화를 파악하는 편이 훨씬 쉽고, 스레드 안전성을 더 쉽게 유지하고 검증할 수 있다.





2.3. 락


-
여러 개의 변수가 하나의 불변조건을 구성하고 있다면, 이 변수들은 서로 독립적이지 않다.
이 변수들은 단일 연산 작업 내에서 함께 변경되어야 한다.


-
상태를 일관성 있게 유지하려면 관련 있는 변수들을 하나의 단일 연산으로 갱신해야 한다.


-
메소드 선언 부에 synchronized 키워드를 지정하면, 메소드 내부의 코드 전체를 포함하면서 메소드가 포함된 클래스의 인스턴스를 락으로 사용하는 synchronized 블록을 간략하게 표현한 것으로 볼 수 있다.


cf) static synchronized 메소드는 해당 class 객체를 락으로 사용한다.


-
synchronized 와 같은 락을 암묵적인 락(intrinsic lock) 또는 모니터 락(monitor lock)이라고 한다.


-
자바에서 암묵적인 락은 뮤텍스(mutexes, mutual exclusion lock(상호 배제 락))으로 동작한다.
즉 한 번에 한 스레드만 특정 락을 소유할 수 있다.


-
스레드가 다른 스레드가 가진 락을 요청하면 해당 스레드는 대기 상태에 들어간다.
암묵적인 락은 재진입 가능(reentrant) 하기 때문에 특정 스레드가 자기가 이미 획득한 락을 다시 확보할 수 있다.
재진입성은 확보 요청 단위가 아닌 스레드 단위로 락을 얻는다는 것을 의미한다. ( pThreads (POSIX 스레드) 의 기본 락은 확보 요청 기준이다. )



2.4. 락으로 상태 보호하기


-
특정 변수에 대한 접근을 조율하기 위해 동기화할 때는 해당 변수에 접근하는 모든 부분을 동기화해야 한다.
또한 변수에 대한 접근을 조율하기 위해 락을 사용할 땐 해당 변수에 접근하는 모든 곳에서 반드시 같은 락을 사용해야 한다.
흔한 실수 중 하나는 공유 변수에 값을 쓸 때만 동기화가 필요하다고 생각하기 쉽다는 점인데, 당연하지만 잘못된 생각이다.


-
여러 스레드에서 접근할 수 있고 변경 가능한 모든 변수를 대상으로 해당 변수에 접근할 때는 항상 동일한 락을 먼저 확보한 상태여야 한다.
이 경우 해당 변수는 확보된 락에 의해 보호된다고 말한다.
( 락으로 보호돼 있다는 사실은 @GuardedBy annotation 으로 표시하는 것이 좋다. )


-
모든 변경할 수 있는 공유 변수는 정확하게 단 하나의 락으로 보호해야 한다.
유지 보수하는 사람이 알 수 있게 어느 락으로 보호하고 있는지를 명확하게 표시하라.


-
락을 활용함에 있어 일반적인 사용 예는 먼저 모든 변경 가능한 변수를 객체 안에 캡슐화하고, 해당 객체의 암묵적인 락을 사용해 캡슐화한 변수에 접근하는 모든 코드경로를 동기화함으로써 여러 스레드가 동시에 접근하는 상태에서 내부 변수를 보호하는 방법이다.


-
여러 변수에 대한 불변조건이 있으면 해당 변수들은 모두 같은 락으로 보호해야 한다.


-
모든 메소드를 동기화하면 활동성이나 성능에 문제가 생길 수도 있다.



2.5. 활동성과 성능


-
락을 얻고 놓는 작업만으로도 어느 정도의 부하가 따른다.
따라서 단일 연산 구조에 문제가 생기지 않는다 해도 synchronized 블록을 "너무 잘게" 쪼개는 일은 바람직하지 않다.


-
종종 단순성과 성능이 서로 상충할 때가 있다.
동기화 정책을 구현할 때는 성능을 위해 조급하게 단순성(잠재적으로 안전성을 훼손하면서)을 희생하고픈 유혹을 버려야 한다.


-
복잡하고 오래 걸리는 계산 작업, 네트웍 작업, 사용자 입출력 작업과 같이 빨리 끝나지 않을 수 있는 작업을 하는 부분에서는 가능한 한 락을 잡지 말아라.






반응형

댓글