JavaRush /Java Blog /Random-KO /변동성 관리
lexmirnov
레벨 29
Москва

변동성 관리

Random-KO 그룹에 게시되었습니다

휘발성 변수 사용 지침

브라이언 게츠(Brian Goetz) 2007년 6월 19일 원본: 변동성 관리 Java의 휘발성 변수는 "동기화-라이트"라고 불릴 수 있습니다. 동기화된 블록보다 사용하는 데 더 적은 코드가 필요하고 종종 더 빠르게 실행되지만 동기화된 블록이 수행하는 작업의 일부만 수행할 수 있습니다. 이 기사에서는 휘발성을 효과적으로 사용하기 위한 몇 가지 패턴과 이를 사용하지 말아야 할 곳에 대한 몇 가지 경고를 제시합니다. 잠금에는 상호 배제(뮤텍스)와 가시성의 두 가지 주요 기능이 있습니다. 상호 배제는 잠금이 한 번에 하나의 스레드에 의해서만 유지될 수 있음을 의미하며, 이 속성은 한 번에 하나의 스레드만 사용하도록 공유 리소스에 대한 액세스 제어 프로토콜을 구현하는 데 사용될 수 있습니다. 가시성은 보다 미묘한 문제이며, 그 목적은 잠금이 해제되기 전에 공용 리소스에 대한 변경 사항이 해당 잠금을 인계받는 다음 스레드에 표시되도록 하는 것입니다. 동기화가 가시성을 보장하지 않으면 스레드가 공용 변수에 대해 오래되거나 잘못된 값을 수신할 수 있으며 이로 인해 여러 가지 심각한 문제가 발생할 수 있습니다.
휘발성 변수
휘발성 변수는 동기화된 변수의 가시성 속성을 가지지만 원자성은 부족합니다. 이는 스레드가 자동으로 휘발성 변수의 최신 값을 사용한다는 것을 의미합니다. 스레드 안전성을 위해 사용될 수 있지만 매우 제한된 경우, 즉 여러 변수 사이 또는 변수의 현재 값과 미래 값 사이의 관계를 도입하지 않는 경우입니다 . 따라서 휘발성만으로는 카운터, 뮤텍스 또는 불변 부분이 여러 변수와 연결된 클래스(예: "start <=end")를 구현하는 데 충분하지 않습니다. 단순성 또는 확장성이라는 두 가지 주요 이유 중 하나로 휘발성 잠금을 선택할 수 있습니다. 일부 언어 구성은 잠금 대신 휘발성 변수를 사용하면 프로그램 코드로 작성하기가 더 쉽고 나중에 읽고 이해하기가 더 쉽습니다. 또한 잠금과 달리 스레드를 차단할 수 없으므로 확장성 문제가 발생할 가능성이 적습니다. 쓰기보다 읽기가 더 많은 상황에서는 휘발성 변수가 잠금에 비해 성능상의 이점을 제공할 수 있습니다.
휘발성 물질의 올바른 사용을 위한 조건
제한된 상황에서는 잠금을 휘발성 잠금으로 교체할 수 있습니다. 스레드로부터 안전하려면 두 가지 기준을 모두 충족해야 합니다.
  1. 변수에 기록되는 내용은 현재 값과 무관합니다.
  2. 변수는 다른 변수와의 불변성에 참여하지 않습니다.
간단히 말해서, 이러한 조건은 휘발성 변수에 쓸 수 있는 유효한 값이 변수의 현재 상태를 포함하여 프로그램의 다른 모든 상태와 무관하다는 것을 의미합니다. 첫 번째 조건은 스레드로부터 안전한 카운터로 휘발성 변수를 사용하는 것을 제외합니다. 증분(x++)은 단일 작업처럼 보이지만 실제로는 휘발성이 제공하지 않는 원자적으로 수행되어야 하는 읽기-수정-쓰기 작업의 전체 시퀀스입니다. 유효한 작업을 위해서는 x 값이 작업 전반에 걸쳐 동일하게 유지되어야 하는데, 이는 휘발성을 사용하여 달성할 수 없습니다. (단, 하나의 스레드에서만 값을 쓰는 것이 보장된다면 첫 번째 조건은 생략할 수 있습니다.) 대부분의 상황에서 첫 번째 또는 두 번째 조건이 위반되므로 휘발성 변수는 동기화된 변수보다 스레드 안전성을 달성하기 위해 덜 일반적으로 사용되는 접근 방식이 됩니다. 목록 1은 숫자 범위가 포함된 스레드로부터 안전하지 않은 클래스를 보여줍니다. 여기에는 불변이 포함됩니다. 하한은 항상 상한보다 작거나 같습니다. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } 범위 상태 변수는 이러한 방식으로 제한되므로 클래스가 스레드로부터 안전한지 확인하기 위해 하위 및 상위 필드를 휘발성으로 만드는 것만으로는 충분하지 않습니다. 여전히 동기화가 필요합니다. 그렇지 않으면 조만간 운이 좋지 않을 것이며 부적절한 값으로 setLower() 및 setUpper()를 수행하는 두 개의 스레드로 인해 범위가 일관되지 않은 상태로 이어질 수 있습니다. 예를 들어 초기 값이 (0, 5)이고 스레드 A가 setLower(4)를 호출하고 동시에 스레드 B가 setUpper(3)를 호출하는 경우 이러한 인터리브 작업은 둘 다 검사를 통과하더라도 오류가 발생합니다. 그것은 불변성을 보호해야합니다. 결과적으로 범위는 (4, 3) - 잘못된 값이 됩니다. 다른 범위 작업에 대해 setLower() 및 setUpper()를 원자적으로 만들어야 합니다. 필드를 휘발성으로 만들면 그렇게 되지 않습니다.
성능 고려 사항
휘발성을 사용하는 첫 번째 이유는 단순성입니다. 어떤 상황에서는 그러한 변수를 사용하는 것이 관련된 잠금을 사용하는 것보다 더 쉽습니다. 두 번째 이유는 성능입니다. 때로는 휘발성이 잠금보다 빠르게 작동합니다. 특히 Java Virtual Machine의 내부 작업과 관련하여 "X는 항상 Y보다 빠릅니다"와 같은 정확하고 포괄적인 설명을 작성하는 것은 극히 어렵습니다. (예를 들어, JVM은 일부 상황에서 잠금을 완전히 해제할 수 있으므로 휘발성 대 동기화 비용을 추상적인 방식으로 논의하기가 어렵습니다.) 그러나 대부분의 최신 프로세서 아키텍처에서 휘발성을 읽는 비용은 일반 변수를 읽는 비용과 크게 다르지 않습니다. 휘발성을 작성하는 비용은 가시성에 필요한 메모리 펜싱으로 인해 일반 변수를 작성하는 것보다 훨씬 높지만 일반적으로 잠금을 설정하는 것보다 저렴합니다.
휘발성의 올바른 사용을 위한 패턴
많은 동시성 전문가들은 휘발성 변수가 잠금보다 올바르게 사용하기가 더 어렵기 때문에 사용을 피하는 경향이 있습니다. 그러나 주의 깊게 따르면 다양한 상황에서 안전하게 사용할 수 있는 몇 가지 잘 정의된 패턴이 있습니다. 항상 휘발성의 한계를 존중하십시오. 프로그램의 다른 어떤 것과도 독립적인 휘발성 물질만 사용하십시오. 이렇게 하면 이러한 패턴으로 인해 위험한 영역에 빠지는 것을 방지할 수 있습니다.
패턴 #1: 상태 플래그
아마도 변경 가능한 변수의 정식 사용은 초기화 완료 또는 종료 요청과 같은 중요한 일회성 수명 주기 이벤트가 발생했음을 나타내는 간단한 부울 상태 플래그일 것입니다. 많은 애플리케이션에는 목록 2에 표시된 것처럼 "종료할 준비가 될 때까지 계속 실행" 형식의 제어 구성이 포함되어 있습니다. volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } shutdown() 메서드는 루프 외부의 다른 스레드에서 호출될 가능성이 높습니다. 따라서 올바른 변수 가시성 shutdownRequested를 보장하려면 동기화가 필요합니다. (JMX 리스너, GUI 이벤트 스레드의 액션 리스너, RMI, 웹 서비스 등을 통해 호출할 수 있습니다.) 그러나 동기화된 블록이 있는 루프는 목록 2와 같이 휘발성 상태 플래그가 있는 루프보다 훨씬 더 번거롭습니다. 휘발성을 사용하면 코드 작성이 더 쉬워지고 상태 플래그가 다른 프로그램 상태에 의존하지 않기 때문에 이는 다음의 예입니다. 휘발성을 잘 사용합니다. 이러한 상태 플래그의 특징은 일반적으로 상태 전환이 하나만 있다는 것입니다. shutdownRequested 플래그가 false에서 true로 바뀌고 프로그램이 종료됩니다. 이 패턴은 앞뒤로 변경될 수 있는 상태 플래그로 확장될 수 있지만 외부 개입 없이 전환 주기(false에서 true, false로)가 발생하는 경우에만 가능합니다. 그렇지 않으면 원자 변수와 같은 일종의 원자 전환 메커니즘이 필요합니다.
패턴 #2: 일회성 보안 게시
동기화가 없을 때 발생할 수 있는 가시성 오류는 기본 값 대신 개체 참조를 작성할 때 훨씬 더 어려운 문제가 될 수 있습니다. 동기화하지 않으면 다른 스레드에서 작성한 개체 참조의 현재 값을 볼 수 있으며 해당 개체의 오래된 상태 값도 계속 볼 수 있습니다. (이 위협은 동기화 없이 객체 참조를 읽는 악명 높은 이중 확인 잠금 문제의 근본 원인입니다. 그러면 실제 참조를 볼 수 있지만 이를 통해 부분적으로 구성된 객체를 얻을 위험이 있습니다.) 객체는 휘발성 객체를 참조하는 것입니다. Listing 3은 시작하는 동안 백그라운드 스레드가 데이터베이스에서 일부 데이터를 로드하는 예를 보여줍니다. 이 데이터를 사용하려고 시도할 수 있는 다른 코드는 사용을 시도하기 전에 해당 데이터가 게시되었는지 확인합니다. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } theFooble에 대한 참조가 일시적이지 않은 경우 doWork()의 코드는 theFlooble을 참조하려고 할 때 부분적으로 생성된 Flooble을 볼 위험이 있습니다. 이 패턴의 핵심 요구 사항은 게시된 개체가 스레드로부터 안전하거나 사실상 불변이어야 한다는 것입니다. 즉, 불변이란 게시된 후 상태가 절대 변경되지 않는다는 의미입니다. 휘발성 링크를 사용하면 개체가 게시된 형식으로 표시되도록 할 수 있지만 게시 후 개체의 상태가 변경되면 추가 동기화가 필요합니다.
패턴 #3: 독립적인 관찰
휘발성을 안전하게 사용하는 또 다른 간단한 예는 관찰 결과가 프로그램 내에서 사용하기 위해 주기적으로 "게시"되는 경우입니다. 예를 들어, 현재 온도를 감지하는 환경 센서가 있습니다. 백그라운드 스레드는 몇 초마다 이 센서를 읽고 현재 온도가 포함된 휘발성 변수를 업데이트할 수 있습니다. 그러면 다른 스레드는 이 변수를 읽을 수 있으며 그 값이 항상 최신임을 알 수 있습니다. 이 패턴의 또 다른 용도는 프로그램에 대한 통계를 수집하는 것입니다. 목록 4는 인증 메커니즘이 마지막으로 로그인한 사용자의 이름을 기억하는 방법을 보여줍니다. lastUser 참조는 프로그램의 나머지 부분에서 사용할 값을 게시하는 데 재사용됩니다. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } 이 패턴은 이전 패턴을 확장합니다. 값은 프로그램의 다른 곳에서 사용하기 위해 게시되지만 게시는 일회성 이벤트가 아니라 일련의 독립적인 이벤트입니다. 이 패턴을 사용하려면 게시된 값이 효과적으로 변경 불가능해야 합니다. 즉, 게시 후에 상태가 변경되지 않아야 합니다. 값을 사용하는 코드는 언제든지 변경될 수 있다는 점을 인식해야 합니다.
패턴 #4: "휘발성 콩" 패턴
"휘발성 빈" 패턴은 JavaBeans를 "영광화된 구조체"로 사용하는 프레임워크에 적용 가능합니다. "휘발성 빈" 패턴은 getter 및/또는 setter가 있는 독립적인 속성 그룹에 대한 컨테이너로 JavaBean을 사용합니다. "휘발성 빈" 패턴의 이론적 근거는 많은 프레임워크가 변경 가능한 데이터 홀더(예: HttpSession)에 대한 컨테이너를 제공하지만 이러한 컨테이너에 배치된 개체는 스레드로부터 안전해야 한다는 것입니다. 휘발성 빈 패턴에서 모든 JavaBean 데이터 요소는 휘발성이며 getter 및 setter는 간단해야 합니다. 해당 속성을 가져오거나 설정하는 것 이외의 논리를 포함해서는 안 됩니다. 또한 객체 참조인 데이터 멤버의 경우 해당 객체는 사실상 불변이어야 합니다. (배열 참조가 휘발성으로 선언되면 요소 자체가 아닌 해당 참조만 휘발성 속성을 갖기 때문에 배열 참조 필드를 허용하지 않습니다.) 모든 휘발성 변수와 마찬가지로 JavaBeans의 속성과 관련된 불변이나 제한이 있을 수 없습니다. . "휘발성 빈" 패턴을 사용하여 작성된 JavaBean의 예가 목록 5에 표시되어 있습니다. @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
더욱 복잡한 휘발성 패턴
이전 섹션의 패턴은 휘발성 사용이 합리적이고 분명한 일반적인 경우의 대부분을 다룹니다. 이 섹션에서는 휘발성이 성능이나 확장성 이점을 제공할 수 있는 보다 복잡한 패턴을 살펴봅니다. 보다 발전된 휘발성 패턴은 매우 취약할 수 있습니다. 가정을 주의 깊게 문서화하고 이러한 패턴을 강력하게 캡슐화하는 것이 중요합니다. 아주 작은 변경이라도 코드를 손상시킬 수 있기 때문입니다! 또한 더 복잡한 휘발성 사용 사례의 주된 이유는 성능이므로 이를 사용하기 전에 실제로 의도한 성능 향상에 대한 명확한 필요성이 있는지 확인하십시오. 이러한 패턴은 가능한 성능 향상을 위해 가독성이나 유지 관리 용이성을 희생하는 절충안입니다. 성능 향상이 필요하지 않거나 엄격한 측정 프로그램으로 필요하다는 것을 증명할 수 없다면 아마도 나쁜 거래일 것입니다. 당신은 가치 있는 것을 포기하고 그 대가로 더 적은 것을 얻고 있습니다.
패턴 #5: 저렴한 읽기-쓰기 잠금
지금쯤이면 휘발성이 카운터를 구현하기에는 너무 약하다는 것을 잘 알고 계실 것입니다. ++x는 본질적으로 세 가지 작업(읽기, 추가, 저장)을 줄여서 문제가 발생하는 경우 여러 스레드가 동시에 휘발성 카운터를 증가시키려고 하면 업데이트된 값을 잃게 됩니다. 그러나 변경 사항보다 읽기 횟수가 훨씬 더 많은 경우에는 내장 잠금 및 휘발성 변수를 결합하여 전체 코드 경로 오버헤드를 줄일 수 있습니다. Listing 6에서는 동기화를 사용하여 증분 작업이 원자성인지 확인하고 휘발성을 사용하여 현재 결과가 표시되는지 확인하는 스레드로부터 안전한 카운터를 보여줍니다. 업데이트가 자주 발생하지 않는 경우 읽기 비용이 일시적인 읽기로 제한되므로 일반적으로 충돌하지 않는 잠금을 획득하는 것보다 저렴하므로 이 접근 방식을 사용하면 성능이 향상될 수 있습니다. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } 이 방법을 "저렴한 읽기-쓰기 잠금"이라고 부르는 이유는 읽기와 쓰기에 서로 다른 타이밍 메커니즘을 사용하기 때문입니다. 이 경우 쓰기 작업은 휘발성 사용의 첫 번째 조건을 위반하므로 휘발성을 사용하여 카운터를 안전하게 구현할 수 없습니다. 잠금을 사용해야 합니다. 그러나 휘발성을 사용하여 읽을 때 현재 값을 표시할 수 있으므로 모든 수정 작업에는 잠금을 사용하고 읽기 전용 작업에는 휘발성을 사용합니다. 잠금이 한 번에 하나의 스레드만 값에 액세스하도록 허용하는 경우 휘발성 읽기는 둘 이상의 읽기를 허용하므로 읽기를 보호하기 위해 휘발성을 사용하면 모든 코드에 잠금을 사용할 때보다 더 높은 수준의 교환을 얻을 수 있습니다. 읽고 기록합니다. 그러나 이 패턴의 취약성에 유의하십시오. 두 가지 경쟁 동기화 메커니즘을 사용하면 이 패턴의 가장 기본적인 적용 범위를 넘어서면 매우 복잡해질 수 있습니다.
요약
휘발성 변수는 잠금보다 간단하지만 약한 형태의 동기화로, 어떤 경우에는 내장 잠금보다 더 나은 성능이나 확장성을 제공합니다. 휘발성의 안전한 사용을 위한 조건을 충족하는 경우(변수는 다른 변수 및 이전 값 모두와 실제로 독립적입니다) 때때로 동기화를 휘발성으로 대체하여 코드를 단순화할 수 있습니다. 그러나 휘발성을 사용하는 코드는 잠금을 사용하는 코드보다 취약한 경우가 많습니다. 여기에 제안된 패턴은 변동성이 동기화에 대한 합리적인 대안인 가장 일반적인 경우를 다룹니다. 이러한 패턴을 따르고 한계를 초과하지 않도록 주의함으로써 이점을 제공하는 경우 휘발성을 안전하게 사용할 수 있습니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION