JAVA/Concurrency

# Java Concurrency In Practice - Chapter 2

skysoo1111 2020. 2. 13. 18:08

Thread Safety

 

Java 동기화의 기본 메커니즘은 synchronized 키워드이지만 "synchronized (동기화)"라는 용어에는 volatile 변수, exclusive locking (명시적 잠금) 및 atomic 변수의 사용도 포함된다. 이 규칙이 적용되지 않는 "특별한"상황이 있다고 생각하려는 유혹을 피해야한다. Java의 동기화 메커니즘을 사용하지 않더라도 테스트를 통과하고 프로그램이 작동하고 수년 동안 성능이 좋을 수 있지만, 그 프로그램은 여전히 동기화 문제로 인해 언제든지 중단 될 가능성이 있는 위험한 프로그램일 것이다.

 

여러 스레드가 적절한 동기화없이 동일한 변경 가능 상태 변수에 접근하면 프로그램은 예기치 않게 동작할 것이다. 이런 문제를 해결하기 위한 세가지 방법이 존재한다.

  • 스레드간에 상태 변수를 공유하지 마십시오.
  • 상태 변수를 변경 불가능하게 만듭니다.
  • 상태 변수에 접근 할 때마다 동기화를 사용하십시오.

2.1 스레드 안전(Thread Safety)이란 무엇인가?

Thread Safety의 합리적인 정의의 핵심은 정확성 개념이다.

 

정확성이란 클래스가 정의된 사양을 준수함을 의미한다. 여러 스레드가 클래스에 접근 하더라도 계속 올바르고 정확하게 작동 할 때, 스레드로부터 안전하다고 말할 수 있다.

2.1.1 Example: A Stateless Servlet

@ThreadSafe
public class StatelessFactorizer implements Servlet {
 	public void service(ServletRequest req, ServletResponse resp) {
 		BigInteger i = extractFromRequest(req);
 		BigInteger[] factors = factor(i);
 		encodeIntoResponse(resp, factors);
 	}
 }

위 처럼 상태값을 가지고 있지 않는 객체는 다른 스레드의 결과에 영향을 주지 않기 때문에 Thread Safety하다고 말할 수 있다. 즉, 다른 스레드의 작업 정확성에 영향을 미치지 않는다.

 

2.2 원자성 (Atomicity)

 

상태값을 가지지 않는 객체에 상태 요소를 하나 추가하면 어떻게 되나? 아래 클래스는 싱글 스레드에서 제대로 동작할 수 있지만 Thread Safety하다고 말할 수 없다.

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
 	private long count = 0;
 	public long getCount() { return count; }
    
 	public void service(ServletRequest req, ServletResponse resp) {
 		BigInteger i = extractFromRequest(req);
 		BigInteger[] factors = factor(i);
 		++count;
 		encodeIntoResponse(resp, factors);
 }
} 

 

++count의 작업은 count를 증가시키는 단일 액션처럼 보이지만, 실제로는 read, modify, write의 3가지 액션이 이뤄지기 때문에 원자적이지 않다.

 

아래 1.1그림은 위의 클래스에서 두 스레드가 동기화 없이 동시에 count를 증가시키려고 할 때, 발생할 수 있는 상황을 보여준다.

 

 

2.2.1 경쟁 조건 (Race Conditions)

 

아래 LazyInitRace 클래스에서 두 개의 스레드가 동시에 getInstance()를 콜한다고 가정해보자. 두 스레드는 두개의 다른 결과를 리턴할 수 있다. 대부분의 동시성 오류와 마찬가지로 경쟁 조건으로 인해 항상 오류가 발생하지는 않지만, 불행한 타이밍으로 인해 심각한 문제를 일으킬 가능성을 언제나 가지고 있게 된다.

@NotThreadSafe
public class LazyInitRace {
 	private ExpensiveObject instance = null;
 
	 public ExpensiveObject getInstance() {
 		if (instance == null)
			 instance = new ExpensiveObject();
 		return instance;
 	}
} 

 

2.2.3 Compound Actions

 

우리는 위 2.2의 예제에서 UnsafeCountingFactorizer 클래스의 count변수가 ++count 라인에서 원자적이지 않게 동작되면서 Thread Safety하지 않았던 문제를 AtomicLong을 사용함으로써 해결할 수 있다.

@ThreadSafe
public class CountingFactorizer implements Servlet {
 	private final AtomicLong count = new AtomicLong(0);
 	public long getCount() { return count.get(); }
 
 	public void service(ServletRequest req, ServletResponse resp) {
		 BigInteger i = extractFromRequest(req);
		 BigInteger[] factors = factor(i);
 		count.incrementAndGet();
 		encodeIntoResponse(resp, factors);
 } 

 

java.util.concurrent.atomic 패키지에는 숫자 및 오브젝트 참조에서 원자 상태 전이에 영향을 주는 원자 변수 클래스가 포함되어 있다. Long 타입 변수 count를 AtomicLong으로 바꾸면 count 상태에 접근하는 모든 작업이 원자적이게 된다.

 

가능하면 AtomicLong과 같은 스레드 안전 개체를 사용하여 클래스 상태를 관리하는 것이 유지 관리하기가 더 쉽다.

 

2.3 Locking

 

스레드 안전성의 정의는 여러 스레드에서 작업의 타이밍에 관계없이 클래스의 불변 값을 보존해야 한다.

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
 
 	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
        
 		if (i.equals(lastNumber.get()))
 			encodeIntoResponse(resp, lastFactors.get() );
		else {
			BigInteger[] factors = factor(i);
 			lastNumber.set(i);
		 	lastFactors.set(factors);
		 	encodeIntoResponse(resp, factors);
		 }
	 }
} 

 

2.3.1 고유 잠금 (Intrinsic Locks)

synchronized (lock) {
 // Access or modify shared state guarded by lock
} 

모든 Java 객체는 묵시적으로 동기화를 위해 Lock(잠금) 역할을 할 수 있다. 이러한 고유 잠금 장치를 Intrinsic Locks 또는 Monitor Locks 이라고한다. Lock은 동기화 된 블록에 들어가기 전에 실행 스레드에 의해 자동으로 획득되며 제어가 동기화 된 블록을 종료 할 때 자동으로 해제 된다.

 

동기화된 블록은 한번에 하나의 스레드만 진입할 수 있으므로 원자적으로 실행된다. 동시성의 맥락에서 원자성은 트랜잭션과 같은 의미를 가진다.

 

아래 SynchronizedFactorizer 클래스는 동기화 메커니즘을 사용했기 때문에 이제 스레드로부터 안전하다.

@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
 	@GuardedBy("this") private BigInteger lastNumber;
 	@GuardedBy("this") private BigInteger[] lastFactors;
    
 	public synchronized void service(ServletRequest req,ServletResponse resp) {
 		 BigInteger i = extractFromRequest(req);
 		 if (i.equals(lastNumber))
 			encodeIntoResponse(resp, lastFactors);
		 else {
			BigInteger[] factors = factor(i);
 			lastNumber = i;
 			lastFactors = factors;
 			encodeIntoResponse(resp, factors);
		 }
 	}
} 

 

2.3.2 재진입 (Reentrancy)

재진입은 Lock이 호출 당이 아닌 스레드 당으로 획득됨을 의미한다.

 

재진입은 Lock 동작의 캡슐화를 용이하게하여 객체 지향적 동시 코드의 개발을 단순화한다. 재진입 잠금이 없으면 서브 클래스가 동기화 된 메소드를 재정의하고 수퍼 클래스 메소드를 호출하는 아래의 매우 자연스럽게 보이는 코드는 교착 상태가 될 것이다. 

public class Widget {
 	public synchronized void doSomething() {
 		...
 	}
}

public class LoggingWidget extends Widget {
 	public synchronized void doSomething() {
		 System.out.println(toString() + ": calling doSomething");
 		super.doSomething();
 	}
}

 

 

2.4 Guarding State with Locks

위 예제 코드 SynchronizedFactorizer 클래스는 고유 잠금에 의해 보호되며, 그것은 @GuardedBy 주석에 의해 문서화 된다.

모든 객체에 기본 제공 잠금 기능이 있다는 점은 편의상 잠금 객체를 명시 적으로 만들 필요가 없다는 것을 의미한다.

모든 데이터를 여러 스레드에서 액세스 할 수있는 변경 가능한 데이터 만 잠금으로 보호 할 필요는 없다.

모든 메소드를 동기화하면 SynchronizedFactorizer에서 보듯이 라이브 또는 성능 문제가 발생할 수 있다.

 

2.5 Liveness and Performance

그림 2.1은 동기화 된 팩터링 서블릿에 대한 여러 요청이 도착하면 어떻게되는지 보여준다. 이 웹 응용 프로그램은 동시성이 좋지 않아 보인다.

 

동기화 된 블록의 범위를 좁 히면 스레드 안전성을 유지하면서 서블릿의 동시성을 쉽게 개선 할 수 있지만 그렇다고 동기화 블록의 범위를 너무 작게 만들지 않도록 주의해야 한다.

 

 

CachedFactorizer는 클래스는 더 이상 cacheHits 변수에 AtomicLong 타입을 사용하지 않아도 된다. CachedFactorizer의 재구성은 단순성 (전체 방법 동기화)과 동시성 (가장 짧은 코드 경로 동기화) 간의 균형을 제공한다.

 

잠금을 획득하고 해제하는 데 약간의 오버 헤드가 있으므로 원자성이 손상되지 않더라도 동기화 된 블록을 너무 많이 분해하는 것은 바람직하지 않다.

@ThreadSafe
public class CachedFactorizer implements Servlet {
 	@GuardedBy("this") private BigInteger lastNumber;
 	@GuardedBy("this") private BigInteger[] lastFactors;
	 @GuardedBy("this") private long hits;
 	@GuardedBy("this") private long cacheHits;
 	
    public synchronized long getHits() { return hits; }
 	
    public synchronized double getCacheHitRatio() {
 		return (double) cacheHits / (double) hits;
 	}
	
    public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
 		BigInteger[] factors = null;
 		synchronized (this) {
 			++hits;
 			if (i.equals(lastNumber)) {
 				++cacheHits;
 				factors = lastFactors.clone();
 			}
 		}
    
 		if (factors == null) {
	 		factors = factor(i);
	 		synchronized (this) {
	 			lastNumber = i;
	 			lastFactors = factors.clone();
	 		}
		 }
		 encodeIntoResponse(resp, factors);
	 }
} 
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다한국어 -> 영어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다한국어 -> 영어...
       
    • 새로운 단어 목록 생성...
  • 복사

 

 동기화 정책을 구현할 때는 성능을 위해 단순성 (잠재적으로 안전을 저해하는)을 포기하지 마라.

 

  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다한국어 -> 영어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다한국어 -> 영어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사
  • 단어장에 추가
     
    • 다음에 대한 단어 목록이 없습니다영어 -> 한국어...
       
    • 새로운 단어 목록 생성...
  • 복사

volatile 변수와 atomic 변수를 이해하려면 기본적인 Java의 메모리 참조 프로세스를 이해해야 한다.