Skip to content

[아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라 #102

@jupyter471

Description

@jupyter471

📌 [아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라

✨ 핵심 내용

  • synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장

<동기화의 기능>

  • 배타적 실행, 객체에 락을 걸어 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다

  • 한 스레드가 만든 변화를 다른 스레드에서 확인할 수 있게 해준다

    즉, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다

자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다

⇒ 동기화는 스레드 사이의 안정적인 통신에 꼭 필요함!

자바의 메모리 모델

스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정함

공유 중인 가변 데이터를 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 결과가 처참해질 수 있다

  • Thread.stop 메서드는 이미 deprecated 되었다
  • 사용 금지!
public class StopThread {
		private static boolean stopRequested;
		
		public static void main(String[] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
						int i = 0;
						while (!stopRequested)
							i++;
				});
				backgroundTh read.start();
				
				TimeUnit.SECONDS.sleep(1);
				stopRequested = true;
		}
}

스레드1 : boolean 값을 폴링하면서 true가 되면 멈춤

다른 스레드에서 스레드1을 멈추기 위해 필드를 변경하는 방식

프로그램이 1초 후에 종료될거라 예상하지만, 해당 프로그램은 영원히 수행된다!!!

⇒ why? 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다

🔽 수정한 코드 이젠 동기화되어 스레드가 정상 종료된다

public class StopThread {
		private static boolean stopRequested;
		
		private static synchronized void requeststop() {
				stopRequested = true;
		}
		
		private static synchronized boolean stopRequested() {
				return stopRequested;
		}
		
		public static void main(String[] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
						int i = 0;
						while (!stopRequested())
							i++;
				});
				backgroundThread.start();
				TimeUnit.SECONDS.sleep(1);
				requestStop();
		}
}

읽기와 쓰기 메서드 모두 동기화가 필요하다! 쓰기만 동기화해서는 충분하지 않다

🔽 volatile로 선언하면 동기화 생략 가능하다

public class StopThread {
		private static **volatile** boolean stopRequested;
		public static void main(String [] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
				int i = 0;
				while (!stopRequested)
					i++;
				});
				backgroundThread.start();
				
				TimeUnit.SECONDS.sleep(1);
				stopRequested = true;
		}
}

volatile 한정자는 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다! 그렇기에 배타적 수행과는 상관없어도 동기화를 생략가능하다

☠️ ☠️  volatile 주의사항!

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
		return nextSerialNumber++;
}

일련번호 생성 코드

문제는 ++ 연산자이다

++ 연산자는 실제로 nextSerialNumber 해당 필드에 두 번 접근한다

  1. 읽고
  2. 증가한 값을 저장

두번째 스레드가 두 과정 사이를 비집고 들어와 값을 읽어가면, 첫 번째 스레드와 똑같은 값을 돌려받게 된다

프로그램이 잘못된 결과를 계산해내는 오류를 안전 실패라고 함

<해결법>

generateSerialNumber 메서드 그 자체에 synchronized 한정자를 붙이면 된다

→ 동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 됨

  • 다만! synchronized를 붙으면 volatile 는 제거해야함

AtomicLong (아이템 59)

java.util.concurrent.atomic을 이용한 락-프리 동기화

  • 락 없이도 스레드 safe 프로그래밍 지원
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
		return nextSerialNum.getAndlncrement();
}

→ 성능도 동기화보다 우수함!

애초에 가변 데이터를 공유하지 말라!

가변 데이터는 단일 스레드에서만 쓰자!

  • 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화 가능

    → 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값 읽을 수 있음! 이런 객체를 불변이라하고, 이걸 건네는 행위를 안전 발행(safe publication)이라함

  • 방식

    • 클래스 초기화 과정에서 객체를 정적 필드, volatile 필드, final 필드, 락을 통해 접근하는 필드에 저장가능

💡 새롭게 알게 된 점

  • volatile 한정자를 통해 동기화를 간접적으로 할 수 있다는 것을 알게 되었습니다

📚 정리

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고, 쓰는 동작은 반드시 동기화 해야함

배타적 실행은 필요 없고 스레드끼리 통신만 필요하다면 volatile 한정자만으로 동기화 가능

📢 댓글로 각자의 학습 내용을 공유해주세요!

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions